Źródło, do którego odwołuje się OP, ma pewną wiarygodność ... ale co z Microsoftem - jakie jest stanowisko w sprawie korzystania z struct? Szukałem dodatkowej wiedzy od Microsoftu i oto, co znalazłem:
Rozważ zdefiniowanie struktury zamiast klasy, jeśli instancje tego typu są małe i zwykle krótkotrwałe lub są zwykle osadzone w innych obiektach.
Nie definiuj struktury, chyba że typ ma wszystkie następujące cechy:
- Logicznie reprezentuje pojedynczą wartość, podobną do typów pierwotnych (liczba całkowita, podwójna itd.).
- Ma rozmiar instancji mniejszy niż 16 bajtów.
- Jest niezmienny.
- Nie będzie trzeba go często zapakować.
Microsoft konsekwentnie narusza te reguły
Dobra, w każdym razie # 2 i # 3. Nasz ukochany słownik ma 2 struktury wewnętrzne:
[StructLayout(LayoutKind.Sequential)] // default for structs
private struct Entry //<Tkey, TValue>
{
// View code at *Reference Source
}
[Serializable, StructLayout(LayoutKind.Sequential)]
public struct Enumerator :
IEnumerator<KeyValuePair<TKey, TValue>>, IDisposable,
IDictionaryEnumerator, IEnumerator
{
// View code at *Reference Source
}
* Źródło odniesienia
Źródło „JonnyCantCode.com” otrzymało 3 z 4 - całkiem wybaczalne, ponieważ numer 4 prawdopodobnie nie byłby problemem. Jeśli znajdziesz się w boksie struktury, przemyśl swoją architekturę.
Zobaczmy, dlaczego Microsoft użyje tych struktur:
- Każda struktura
Entry
i Enumerator
reprezentuje pojedyncze wartości.
- Prędkość
Entry
nigdy nie jest przekazywany jako parametr poza klasą Dictionary. Dalsze dochodzenie pokazuje, że aby spełnić implementację IEnumerable, Dictionary używa Enumerator
struktury, którą kopiuje za każdym razem, gdy żądany jest moduł wyliczający ... ma sens.
- Wewnętrzny do klasy Dictionary.
Enumerator
jest publiczny, ponieważ słownik jest policzalny i musi mieć równy dostęp do implementacji interfejsu IEnumerator - np. getter IEnumerator.
Aktualizacja - Ponadto należy pamiętać, że gdy struct implementuje interfejs - podobnie jak moduł Enumerator - i jest rzutowany na ten zaimplementowany typ, struct staje się typem referencyjnym i zostaje przeniesiony na stertę. Wewnętrzny do słownika klasie Enumerator jest jeszcze typ wartości. Jednak natychmiast po wywołaniu metody zwracany jest GetEnumerator()
typ odwołania IEnumerator
.
To, czego nie widzimy tutaj, to jakakolwiek próba lub dowód na to, że struktury muszą być niezmienne lub utrzymywać rozmiar instancji tylko 16 bajtów lub mniej:
- Nic w elemencie zadeklarowanych powyżej
readonly
- nie niezmienna
- Rozmiar tych struktur może wynosić znacznie ponad 16 bajtów
Entry
ma nieokreślony czas (od Add()
do Remove()
, Clear()
lub zbieranie śmieci);
I ... 4. Obie struktury przechowują TKey i TValue, o których wszyscy wiemy, że mogą być typami referencyjnymi (dodane informacje o bonusie)
Niezależnie od zaszyfrowanych kluczy, słowniki są częściowo szybkie, ponieważ utworzenie struktury jest szybsze niż typ odwołania. Tutaj mam Dictionary<int, int>
300 000 losowych liczb całkowitych z sekwencyjnie zwiększanymi kluczami.
Pojemność: 312874
MemSize: 2660827 bajtów
Ukończono Zmiana rozmiaru: 5ms
Całkowity czas wypełnienia: 889 ms
Pojemność : liczba elementów dostępnych przed zmianą wewnętrznej tablicy.
MemSize : ustalany przez serializację słownika do MemoryStream i uzyskanie długości bajtu (wystarczająco dokładnej do naszych celów).
Completed Resize : czas potrzebny na zmianę rozmiaru tablicy wewnętrznej z 150862 elementów na 312874 elementów. Kiedy zorientujesz się, że każdy element jest kolejno kopiowany Array.CopyTo()
, nie jest to zbyt nędzne.
Całkowity czas do wypełnienia : wprawdzie wypaczony z powodu logowania i OnResize
wydarzenia, które dodałem do źródła; jednak nadal imponujące, aby wypełnić 300 000 liczb całkowitych, zmieniając rozmiar 15 razy podczas operacji. Z ciekawości, jaki byłby całkowity czas na wypełnienie, gdybym już znał pojemność? 13ms
A co teraz, jeśli Entry
byłaby klasa? Czy te czasy lub wskaźniki naprawdę tak bardzo się różnią?
Pojemność: 312874
MemSize: 2660827 bajtów
Completed Resize: 26ms
Całkowity czas do wypełnienia: 964ms
Oczywiście dużą różnicą jest zmiana rozmiaru. Czy jest jakaś różnica, jeśli słownik zostanie zainicjowany przez pojemność? Za mało, żeby przejmować się ... 12ms .
Dzieje się tak, ponieważ Entry
jest strukturą, nie wymaga inicjalizacji jak typ odwołania. Jest to zarówno piękno, jak i zmora typu wartości. Aby użyć Entry
jako typu odniesienia, musiałem wstawić następujący kod:
/*
* Added to satisfy initialization of entry elements --
* this is where the extra time is spent resizing the Entry array
* **/
for (int i = 0 ; i < prime ; i++)
{
destinationArray[i] = new Entry( );
}
/* *********************************************** */
Powód, dla którego musiałem zainicjować każdy element tablicy Entry
jako typ odwołania, można znaleźć w witrynie MSDN: Structure Design . W skrócie:
Nie udostępniaj domyślnego konstruktora dla struktury.
Jeśli struktura definiuje domyślny konstruktor, podczas tworzenia tablic struktury, środowisko wykonawcze wspólnego języka automatycznie wykonuje domyślny konstruktor dla każdego elementu tablicy.
Niektóre kompilatory, takie jak kompilator C #, nie zezwalają strukturom na domyślne konstruktory.
Jest to właściwie dość proste i zapożyczymy z Trzech praw robotyki Asimova :
- Struktura musi być bezpieczna w użyciu
- Struktur musi skutecznie wykonywać swoją funkcję, chyba że naruszałoby to zasadę nr 1
- Struktura musi pozostać nienaruszona podczas użytkowania, chyba że jej zniszczenie jest wymagane, aby spełnić regułę nr 1
... co bierzemy z tego : krótko mówiąc, bądź odpowiedzialny za stosowanie typów wartości. Są szybkie i wydajne, ale mogą powodować wiele nieoczekiwanych zachowań, jeśli nie są odpowiednio utrzymywane (tj. Niezamierzone kopie).
System.Drawing.Rectangle
narusza wszystkie trzy z tych zasad.