2010-11-12 6 views
15

Pamiętam od samego początku .NET, że wywoływanie ToString na StringBuilderze służyło do dostarczania nowego obiektu napisów (do zwrócenia) z wewnętrznym buforem znaków używanym przez StringBuilder. W ten sposób, jeśli skonstruowałeś ogromny ciąg za pomocą StringBuilder, wywołanie ToString nie musiało go kopiować.Czy StringBuilder staje się niezmienny po wywołaniu ToString?

W ten sposób StringBuilder musiał zapobiec wszelkim dodatkowym zmianom w buforze, ponieważ był używany przez niezmienny ciąg. W rezultacie StringBuilder przełączy się na "copy-on-change", gdzie każda próba zmiany spowoduje najpierw utworzenie nowego bufora, skopiowanie zawartości starego bufora i tylko jego zmianę.

Sądzę, że założono, że StringBuilder zostanie użyty do skonstruowania łańcucha znaków, a następnie przekształcony na zwykły ciąg znaków i odrzucony. Wydaje mi się rozsądnym założeniem.

Teraz o to chodzi. Nie mogę znaleźć żadnej wzmianki o tym w dokumentacji. Ale nie jestem pewien, czy kiedykolwiek został udokumentowany.

Więc spojrzał na realizację ToString użyciu reflektor (.NET 4.0) i wydaje mi się, że to rzeczywiście kopie napisu, a nie tylko dzielić bufor:

[SecuritySafeCritical] 
public override unsafe string ToString() 
{ 
    string str = string.FastAllocateString(this.Length); 
    StringBuilder chunkPrevious = this; 
    fixed (char* str2 = ((char*) str)) 
    { 
     char* chPtr = str2; 
     do 
     { 
      if (chunkPrevious.m_ChunkLength > 0) 
      { 
       char[] chunkChars = chunkPrevious.m_ChunkChars; 
       int chunkOffset = chunkPrevious.m_ChunkOffset; 
       int chunkLength = chunkPrevious.m_ChunkLength; 
       if ((((ulong) (chunkLength + chunkOffset)) > str.Length) ||  (chunkLength > chunkChars.Length)) 
       { 
        throw new ArgumentOutOfRangeException("chunkLength",  Environment.GetResourceString("ArgumentOutOfRange_Index")); 
       } 
       fixed (char* chRef = chunkChars) 
       { 
        string.wstrcpy(chPtr + chunkOffset, chRef, chunkLength); 
       } 
      } 
      chunkPrevious = chunkPrevious.m_ChunkPrevious; 
     } 
     while (chunkPrevious != null); 
    } 
    return str; 
} 

Teraz, jak ja wspomniano wcześniej, wyraźnie pamiętam, że przeczytałem, że tak było na początku, jeśli .NET. Znalazłem nawet wzmiankę o tym book.

Moje pytanie brzmi, czy to zachowanie zostało usunięte? Jeśli tak, to nikt nie wie, dlaczego? To miało dla mnie sens ...

+0

Interesujące. Ciąg jest przechowywany jako seria char [] s. Ale nie ma linii "chunkPrevious = chunkPrevious.m_ChunkPrevious;" sugeruje, że te tablice są przechowywane w oddzielnych instancjach StringBuilder, powiązanych jako lista połączona, wewnętrznie w instancji StringBuilder, do której mamy odnośnik? – Sorax

Odpowiedz

5

Tak, został całkowicie przeprojektowany dla .NET 4.0. Teraz używa liny, połączonej listy konstruktorów stringów do przechowywania rosnącego wewnętrznego bufora. Jest to obejście problemu, gdy nie można odgadnąć początkowej wartości pojemności, a ilość tekstu jest duża. To tworzy wiele kopii niewykorzystanego wewnętrznego bufora, blokując stertę Dużych Przedmiotów. Ten komentarz z kodu źródłowego, który jest dostępny ze Źródła odniesienia, jest istotny:

// We want to keep chunk arrays out of large object heap (< 85K bytes ~ 40K chars) to be sure. 
    // Making the maximum chunk size big means less allocation code called, but also more waste 
    // in unused characters and slower inserts/replaces (since you do need to slide characters over 
    // within a buffer). 
    internal const int MaxChunkSize = 8000; 
0

To był najprawdopodobniej tylko szczegół implementacji, a nie udokumentowane ograniczenie na interfejsie dostarczonym przez StringBuilder.ToString. Fakt, że nie masz pewności, czy kiedykolwiek został udokumentowany, może sugerować, że tak właśnie jest.

Książki często przedstawiają szczegóły implementacji, aby pokazać pewien wgląd w sposób użycia, ale większość nosi ostrzeżenie, że implementacja może ulec zmianie.

Dobry przykład, dlaczego nigdy nie należy polegać na szczegółach implementacji.

Podejrzewam, że nie było cechą, aby budowniczy stał się niezmienny, a jedynie efektem ubocznym wdrożenia ToString.

+1

Dzięki, Jeff. Rozumiem, że był to szczegół implementacji i nie polegam na nim w żaden sposób. To, co mnie interesuje, to dlaczego implementacja uległa zmianie, ponieważ wydaje się, że wciąż ma to sens. –

0

Nie widziałem tego wcześniej, więc oto moje przypuszczenie: wewnętrzne przechowywanie StringBuilder wydaje się już nie być prostym string, ale zbiorem "porcji". ToString nie może zwrócić odwołania do tego wewnętrznego ciągu, ponieważ już nie istnieje.

(wersja Are 4,0 StringBuilders teraz ropes?)

+1

Wygląda raczej jak łańcuch kawałków niż drzewo kawałków. – Guffa

5

Tak, pamiętam. Metoda StringBuilder.ToString służy do zwracania wewnętrznego bufora jako ciągu znaków i oznaczania go jako użytego, aby dodatkowe zmiany w StringBuilder musiały przydzielić nowy bufor.

Ponieważ jest to szczegół implementacji, nie jest to wspomniane w dokumentacji. To dlatego mogą zmieniać podstawową implementację bez naruszania czegokolwiek w zdefiniowanym zachowaniu klasy.

Jak widać z opublikowanego kodu, nie ma już jednego wewnętrznego bufora, zamiast tego znaki są przechowywane w porcjach, a metoda ToString umieszcza porcje razem w łańcuchu.

Przyczyną tej zmiany w implementacji jest prawdopodobnie to, że zebrały informacje o tym, w jaki sposób faktycznie użyto klasy StringBuilder, i dochodzą do wniosku, że takie podejście daje lepszą wydajność ważoną między sytuacjami średnimi a najgorszymi.

+0

StringBuilder przełącza się na zwracanie nowego ciągu znaków w swojej metodzie ToString() na długo przed rozpoczęciem korzystania z lin, gdy Microsoft zdał sobie sprawę, że każdy obiekt, który kiedykolwiek był wystawiony na świat zewnętrzny w celu ochrony przed nićmi, gdy był zmienny, musi na zawsze domniemywa się, że jest zmienny (ponieważ nie ma możliwości sprawdzenia, czy jakiś wątek może być w trakcie pisania obiektu, ale został chwilowo opóźniony z powodu zamiany na dysk, zawieszenia, wywłaszczenia przez wątki o wyższym priorytecie lub cokolwiek innego). – supercat

+1

@supercat: jak długo? IIRC implementacja 2.0 zwróciła wewnętrzny bufor. Ponieważ 3.0 i 3.5 nadal używały kodu 2.0, 4.0 to kolejna wersja. – Guffa

+0

Naprawdę? Pamiętam, że czytałem o zmianie przed laty, zanim 4.0 było na horyzoncie. Myślałem, że zmiana nastąpiła z 2.0; filozofią było to, że jest całkowicie dopuszczalne, aby nie używać nici 'StringBuilder', aby zwrócić ciąg znaków zawierający dowolne znaki śmieci; nie jest w porządku, aby zwrócić ciąg, który może mutować po jego zbadaniu, ponieważ to zachowanie może złamać wiele kodu, który oczekuje, że 'string' będzie niezmienny (pomyśl o efektach wywołania' String.Intern' na łańcuchu, który później mutuje !). – supercat

2

Oto implementacja .NET 1.1 StringBuilder.ToString z reflektorem:

public override string ToString() 
{ 
    string stringValue = this.m_StringValue; 
    int currentThread = this.m_currentThread; 
    if ((currentThread != 0) && (currentThread != InternalGetCurrentThread())) 
    { 
     return string.InternalCopy(stringValue); 
    } 
    if ((2 * stringValue.Length) < stringValue.ArrayLength) 
    { 
     return string.InternalCopy(stringValue); 
    } 
    stringValue.ClearPostNullChar(); 
    this.m_currentThread = 0; 
    return stringValue; 
} 

O ile widzę, to w niektórych przypadkach zwróci ciąg bez kopiowania go. Jednak nie sądzę, że StringBuilder staje się niezmienny. Zamiast tego myślę, że będzie używać kopiowania przy zapisie, jeśli nadal będziesz pisać do StringBuilder.

+0

Dzięki! +1 za dodanie implementacji środowiska .NET 1.1 –

Powiązane problemy