2015-09-08 9 views
14

So che le stringhe sono immutabili e qualsiasi modifica di una stringa crea semplicemente una nuova stringa in memoria (e contrassegna il vecchio come libero). Tuttavia, mi chiedo se la mia logica qui sotto sia corretta, in quanto puoi effettivamente modificare il contenuto di una stringa in un modo round-a-bout.È possibile modificare il contenuto di una stringa (immutabile) tramite un metodo non sicuro?

const string baseString = "The quick brown fox jumps over the lazy dog!"; 

//initialize a new string 
string candidateString = new string('\0', baseString.Length); 

//Pin the string 
GCHandle gcHandle = GCHandle.Alloc(candidateString, GCHandleType.Pinned); 

//Copy the contents of the base string to the candidate string 
unsafe 
{ 
    char* cCandidateString = (char*) gcHandle.AddrOfPinnedObject(); 
    for (int i = 0; i < baseString.Length; i++) 
    { 
     cCandidateString[i] = baseString[i]; 
    } 
} 

Questo approccio infatti modificare i contenuti candidateString (senza creare un nuovo candidateString in memoria) o si fa il runtime vedere attraverso i miei trucchi e trattarlo come una stringa normale?

+1

Esegui il codice e scopri tu stesso cosa succede. – Servy

+0

Perché posso vedere solo i risultati finali e non quello che sta accadendo in memoria. – MagellanTX

+2

Metti un breakpoint sulla riga 'cCandidateString [i] = baseString [i]' e guarda 'candidateString' cambia mentre giochi attraverso ogni iterazione. Cose divertenti! –

risposta

11

Il vostro esempio funziona bene, grazie a diversi elementi:

  • candidateString vive nella memoria dinamica, quindi è sicuro da modificare. Confronta questo con baseString, che è internato. Se si tenta di modificare la stringa internata, potrebbero verificarsi problemi imprevisti. Non c'è alcuna garanzia che la stringa non viva in memoria protetta da scrittura ad un certo punto, anche se sembra funzionare oggi. Sarebbe molto simile all'assegnazione di una stringa costante a una variabile char* in C e quindi alla modifica. In C, è un comportamento indefinito.

  • Si prealloca spazio sufficiente in candidateString - in modo da non sovraccaricare il buffer.

  • dati carattere è non memorizzato all'offset 0 della classe String. È memorizzato con un offset uguale a RuntimeHelpers.OffsetToStringData.

    public static int OffsetToStringData 
    { 
        // This offset is baked in by string indexer intrinsic, so there is no harm 
        // in getting it baked in here as well. 
        [System.Runtime.Versioning.NonVersionable] 
        get { 
         // Number of bytes from the address pointed to by a reference to 
         // a String to the first 16-bit character in the String. Skip 
         // over the MethodTable pointer, & String 
         // length. Of course, the String reference points to the memory 
         // after the sync block, so don't count that. 
         // This property allows C#'s fixed statement to work on Strings. 
         // On 64 bit platforms, this should be 12 (8+4) and on 32 bit 8 (4+4). 
    #if WIN32 
         return 8; 
    #else 
         return 12; 
    #endif // WIN32 
        } 
    } 
    

    Tranne ...

  • GCHandle.AddrOfPinnedObject è speciale carter per due tipi: string e matrice tipi. Invece di restituire l'indirizzo dell'oggetto stesso, giace e restituisce l'offset ai dati. Vedere lo source code in CoreCLR.

    // Get the address of a pinned object referenced by the supplied pinned 
    // handle. This routine assumes the handle is pinned and does not check. 
    FCIMPL1(LPVOID, MarshalNative::GCHandleInternalAddrOfPinnedObject, OBJECTHANDLE handle) 
    { 
        FCALL_CONTRACT; 
    
        LPVOID p; 
        OBJECTREF objRef = ObjectFromHandle(handle); 
    
        if (objRef == NULL) 
        { 
         p = NULL; 
        } 
        else 
        { 
         // Get the interior pointer for the supported pinned types. 
         if (objRef->GetMethodTable() == g_pStringClass) 
          p = ((*(StringObject **)&objRef))->GetBuffer(); 
         else if (objRef->GetMethodTable()->IsArray()) 
          p = (*((ArrayBase**)&objRef))->GetDataPtr(); 
         else 
          p = objRef->GetData(); 
        } 
    
        return p; 
    } 
    FCIMPLEND 
    

In sintesi, il runtime ti permette di giocare con i suoi dati e non si lamenta. Stai usando il codice unsafe dopo tutto. Ho visto di peggio runtime scherzi a parte questo, compresa la creazione di tipi di riferimento sullo stack ;-)

Basta ricordarsi di aggiungere un ulteriore \0dopo tutti i personaggi (all'offset Length) se la stringa finale è più breve di quello che è allocato. Questo non avrà un overflow, ogni stringa ha un carattere null implicito alla fine per facilitare gli scenari di interoperabilità.


Ora date un'occhiata a come StringBuilder crea una stringa, ecco StringBuilder.ToString:

[System.Security.SecuritySafeCritical] // auto-generated 
public override String ToString() { 
    Contract.Ensures(Contract.Result<String>() != null); 

    VerifyClassInvariant(); 

    if (Length == 0) 
     return String.Empty; 

    string ret = string.FastAllocateString(Length); 
    StringBuilder chunk = this; 
    unsafe { 
     fixed (char* destinationPtr = ret) 
     { 
      do 
      { 
       if (chunk.m_ChunkLength > 0) 
       { 
        // Copy these into local variables so that they are stable even in the presence of race conditions 
        char[] sourceArray = chunk.m_ChunkChars; 
        int chunkOffset = chunk.m_ChunkOffset; 
        int chunkLength = chunk.m_ChunkLength; 

        // Check that we will not overrun our boundaries. 
        if ((uint)(chunkLength + chunkOffset) <= ret.Length && (uint)chunkLength <= (uint)sourceArray.Length) 
        { 
         fixed (char* sourcePtr = sourceArray) 
          string.wstrcpy(destinationPtr + chunkOffset, sourcePtr, chunkLength); 
        } 
        else 
        { 
         throw new ArgumentOutOfRangeException("chunkLength", Environment.GetResourceString("ArgumentOutOfRange_Index")); 
        } 
       } 
       chunk = chunk.m_ChunkPrevious; 
      } while (chunk != null); 
     } 
    } 
    return ret; 
} 

Sì, utilizza il codice non sicuro, e sì, è possibile ottimizzare la vostra utilizzando fixed, in quanto questo tipo di pinning è molto più leggero di allocare una maniglia GC:

const string baseString = "The quick brown fox jumps over the lazy dog!"; 

//initialize a new string 
string candidateString = new string('\0', baseString.Length); 

//Copy the contents of the base string to the candidate string 
unsafe 
{ 
    fixed (char* cCandidateString = candidateString) 
    { 
     for (int i = 0; i < baseString.Length; i++) 
      cCandidateString[i] = baseString[i]; 
    } 
} 

Quando usi fixed, il GC scopre solo che un oggetto deve essere bloccato quando incappa su di esso durante una raccolta. Se non ci sono raccolte in corso, il GC non è nemmeno coinvolto. Quando si utilizza GCHandle, ogni handle viene registrato nel GC.

+0

Anche le stringhe internate non vivono nell'heap gestito? Certamente la modifica di una stringa internata sarebbe negativa, perché è molto probabile che anche altri utenti di quella stringa abbiano riferimenti ad essa, cambierà il codice hash della stringa, ecc., Ma pensavo che fosse tutta questione di struttura dei dati piuttosto che di oggetto posizione di stoccaggio. – Random832

+1

@ Random832 sì, ma il mio punto è che è meno sicuro a causa del fatto che il runtime * potrebbe in futuro * archiviare quelli in memoria protetta da scrittura (ad esempio l'immagine exe mappata in memoria da dove provengono). Oggi non sembra protetto da scrittura, ma nulla garantisce che durerà per sempre. –

0

Come altri hanno sottolineato, la mutazione degli oggetti String è utile in alcuni rari casi. Di seguito un esempio con un utile snippet di codice.

caso d'uso/sfondo

Anche se tutti dovrebbero essere un grande fan del personaggio davvero eccellente codifica supporto che .NET ha sempre offerto, a volte potrebbe essere preferibile per ridurre il carico di, in particolare se si esegue molto roundtrapping tra caratteri (legacy) a 8 bit e stringhe gestite (ovvero in genere scenari di interoperabilità).

Come ho accennato, .NET è particolarmente enfatico che è necessario specificare esplicitamente un testo Encoding per qualsiasi/tutte le conversioni di dati di carattere non Unicode per/da oggetti String gestiti. Questo controllo rigoroso alla periferia è davvero encomiabile, dal momento che garantisce che una volta che hai la stringa all'interno del runtime gestito non ti devi preoccupare; tutto è solo ampio Unicode. Anche l'UTF-8 è in gran parte bandito in questo regno incontaminato.

(Per contrasto, richiamano un certo altro linguaggio di scripting popolare che notoriamente pasticciato questo settore, con conseguente alla fine in molti anni di paralleli 2.x e 3.x versioni, tutte a causa di grandi cambiamenti Unicode in quest'ultimo.)

Quindi, .NET sposta tutto quel casino al limite di interoperabilità, forzando Unicode (UTF-16) una volta dentro, ma questa filosofia implica che il lavoro di codifica/decodifica fatto ("una volta per tutte") sia rigorosamente rigoroso, e per questo motivo le classi .NET Encoding/Encoder possono rappresentare un collo di bottiglia per le prestazioni. Se stai spostando molto testo da ampio (Unicode) a semplice fisso ANSI a 7 o 8 bit, ASCII, ecc. (Nota che non sto parlando di MBCS o UTF-8, dove vorresti usare gli Encoder!), il paradigma di codifica .NET potrebbe sembrare eccessivo.

Inoltre, è possibile che non si sappia o non si preoccupi di specificare Encoding. Forse tutto quello che ti interessa è un round-trip veloce e preciso per quel low-byte di un 1623 bit. Se si look at the .NET source code, anche il System.Text.ASCIIEncoding potrebbe essere troppo ingombrante in alcune situazioni.


Il frammento di codice ...

sottile corda: caratteri a 8-bit direttamente memorizzati in un gestito String, un 'char sottile' per carattere Unicode ampia, senza preoccuparsi Tipiche codifica/decodifica durante tutto l'intervento .

Tutti questi metodi ignorano/strisciano il byte superiore di ciascun carattere Unicode a 16 bit, trasmettendo solo esattamente ogni byte basso esattamente così com'è. Ovviamente, il ripristino riuscito del testo Unicode dopo un round trip sarà possibile solo se quei bit superiori non sono rilevanti.

/// <summary> Convert byte array to "thin string" </summary> 
public static unsafe String ToThinString(this byte[] src) 
{ 
    int c; 
    var ret = String.Empty; 
    if ((c = src.Length) > 0) 
     fixed (char* dst = (ret = new String('\0', c))) 
      do 
       dst[--c] = (char)src[c]; // fill new String by in-situ mutation 
      while (c > 0); 

    return ret; 
} 

Nella direzione appena mostrato, che è tipicamente portando dati nativi in a gestito, spesso non hanno l'array di byte gestito, quindi piuttosto che allocare una temporanea solo per lo scopo di chiamando questa funzione, è possibile elaborare i byte nativi grezzi direttamente in una stringa gestita. Come prima, questo ignora tutta la codifica dei caratteri.

I (ovvie) Campo di controlli che sarebbero necessari in questa funzione non sicuri sono eliso per chiarezza:

public static unsafe String ToThinString(byte* pSrc, int c) 
{ 
    var ret = String.Empty; 
    if (c > 0) 
     fixed (char* dst = (ret = new String('\0', c))) 
      do 
       dst[--c] = (char)pSrc[c]; // fill new String by in-situ mutation 
      while (c > 0); 

    return ret; 
} 

Il vantaggio di String mutazione qui è che si evita di allocazioni temporanee scrivendo direttamente alla destinazione finale. Anche se dovessi evitare l'allocazione extra usando stackalloc, ci sarebbe una copia non necessaria dell'intera cosa quando chiamerai il costruttore String(Char*, int, int): chiaramente non c'è modo di associare i dati che hai appena faticosamente preparato con un oggetto String che non ha esisterà finché non avrai finito!


Per completezza ...

Ecco lo specchio-codice che inverte il funzionamento di tornare un array di byte (anche se questa direzione non accade per illustrare la tecnica di stringa-mutazione). Questa è la direzione che normalmente utilizzi per inviare il testo Unicode su del runtime gestito .NET, per l'utilizzo da parte di un'app legacy.

/// <summary> Convert "thin string" to byte array </summary> 
public static unsafe byte[] ToByteArr(this String src) 
{ 
    int c; 
    byte[] ret = null; 
    if ((c = src.Length) > 0) 
     fixed (byte* dst = (ret = new byte[c])) 
      do 
       dst[--c] = (byte)src[c]; 
      while (c > 0); 

    return ret ?? new byte[0]; 
} 
Problemi correlati