2015-08-10 5 views
9

Rozważmy następujący kod:Czy ciągi .NET naprawdę powinny być uważane za niezmienne?

unsafe 
{ 
    string foo = string.Copy("This can't change"); 

    fixed (char* ptr = foo) 
    { 
     char* pFoo = ptr; 
     pFoo[8] = pFoo[9] = ' '; 
    } 

    Console.WriteLine(foo); // "This can change" 
} 

Stwarza to wskaźnik do pierwszego znaku foo, przypisuje go stać zmienny i zmienia znaki 8 i 9 pozycji w górę do ' '.

Uwaga Nigdy właściwie nie przypisano foo; zamiast tego zmieniłem jego wartość modyfikując jej stan lub mutujący łańcuch. Dlatego ciągi .NET są zmienne.

To działa tak dobrze, w rzeczywistości, że następujący kod:

unsafe 
{ 
    string bar = "Watch this"; 

    fixed (char* p = bar) 
    { 
     char* pBar = p; 
     pBar[0] = 'C'; 
    } 

    string baz = "Watch this"; 
    Console.WriteLine(baz); // Unrelated, right? 
} 

wypisze "Catch this" powodu strun dosłownym internowania.

ta ma wiele zastosowań, stosowanych na przykład w ten sposób:

string GetForInputData(byte[] inputData) 
{ 
    // allocate a mutable buffer... 
    char[] buffer = new char[inputData.Length]; 

    // fill the buffer with input data 

    // ...and a string to return 
    return new string(buffer); 
} 

zostaje zastąpiony przez:

string GetForInputData(byte[] inputData) 
{ 
    // allocate a string to return 
    string result = new string('\0', inputData.Length); 

    fixed (char* ptr = result) 
    { 
     // fill the result with input data 
    } 

    return result; // return it 
} 

To może zaoszczędzić potencjalnie ogromne koszty alokacji pamięci/wydajność, jeśli pracujesz w Speed- pole krytyczne (np. kodowanie).

Myślę, że można powiedzieć, że to się nie liczy, ponieważ "używa hacka", aby wskaźniki były zmienne, ale znowu to projektanci języka C#, którzy w pierwszej kolejności wspierali przypisywanie łańcucha do wskaźnika. (W rzeczywistości, to odbywa allthetime wewnętrznie String i StringBuilder, więc technicznie można zrobić własny StringBuilder z tym.)

Tak, należy NET ciągi naprawdę uznać niezmienne?

+0

Są one niezmienne w przypadku korzystania z publicznego interfejsu API. Jeśli używasz niebezpiecznego kodu lub odbicia, aby ominąć ten publiczny interfejs API, nie jest. – MarcinJuraszek

+0

@MarcinJuraszek Wskaźniki * są * częścią publicznego interfejsu API, zobacz także mój ostatni akapit. –

+1

Mówię o publicznym API klasy 'string' - o metodach, właściwościach, które eksponuje. – MarcinJuraszek

Odpowiedz

6

§ 18.6 specyfikacji # języka C (fixed oświadczenie) wyraźnie odnosi się do przypadku modyfikacji ciąg dzięki stałym wskaźnikiem, i wskazuje, że może to doprowadzić do nieokreślonego zachowania:

Modyfikowanie obiektów typu zarządzanego przez stałe wskaźniki mogą powodować niezdefiniowane zachowanie. Na przykład, ponieważ ciągi znaków są niezmienne, obowiązkiem programisty jest upewnienie się, że znaki odwoływane przez wskaźnik do stałego łańcucha nie są modyfikowane.

+0

Interesujące, słyszałem tylko termin "niezdefiniowane zachowanie" używany w specyfikacji C/C++ (przez cały czas). Zobaczenie go w C# jest czymś nowym. –

+1

@JamesKo Istnieje nawet nieokreślone zachowanie w specyfikacji C#, które nie jest związane z kodem "niebezpiecznym" (jedynym, który mogłem znaleźć w szybkim wyszukiwaniu): jeśli używasz niestandardowych awaiterów z 'async' /' await' , a twój niestandardowy awatar oczekuje wiele razy na kontynuację, zachowanie jest niezdefiniowane. – hvd

+0

* To * jest podstawą do postawienia tego pytania ...odpowiedź jest wyraźnie ujęta w specyfikacji językowej i najwyraźniej została uznana przez autorów za tak prawdopodobną kwestię niepokoju, myśleli o niej z dużym wyprzedzeniem. –

1

po prostu musiałem grać z tym i doświadczenia w celu potwierdzenia, czy adresy ciąg dosłownym są skierowane w tym samym miejscu pamięci.

Wyniki są następujące:

string foo = "Fix value?"; //New address: 0x02b215f8 
string foo2 = "Fix value?"; //Points to same address: 0x02b215f8 
string fooCopy = string.Copy(foo); //New address: 0x021b2888 

fixed (char* p = foo) 
{ 
    p[9] = '!'; 
} 

Console.WriteLine(foo); 
Console.WriteLine(foo2); 
Console.WriteLine(fooCopy); 

//Reference is equal, which means refering to same memory address 
Console.WriteLine(string.ReferenceEquals(foo, foo2)); //true 

//Reference is not equal, which creates another string in new memory address 
Console.WriteLine(string.ReferenceEquals(foo, fooCopy)); //false 

Widzimy, że foo inicjuje ciąg dosłownego który wskazuje 0x02b215f8 adresu pamięci w moim komputerze. Przypisanie tego samego ciągu literowego do foo2 odnosi się do tego samego adresu pamięci. A tworzenie kopii tego samego ciągu literalnego tworzy nowy.Dalsze testowanie przez string.ReferenceEquals() ujawnia, że ​​są one rzeczywiście równe dla foo i foo2, podczas gdy inne odniesienia dla foo i fooCopy.

Interesujące jest to, jak można manipulować ciągami literowymi i wpływa na inne zmienne, które się do nich odwołują. Jedną z rzeczy, na które powinniśmy uważać, ponieważ takie zachowanie istnieje.