Kiedy używać struct?


1390

Kiedy należy używać struct, a nie klasy w C #? Mój model koncepcyjny polega na tym, że struktury są używane w czasach, gdy element jest jedynie zbiorem typów wartości . Sposób na logiczne połączenie ich wszystkich w spójną całość.

Natknąłem się na te zasady tutaj :

  • Struktura powinna reprezentować pojedynczą wartość.
  • Struktura powinna mieć ślad w pamięci mniejszy niż 16 bajtów.
  • Struktury nie należy zmieniać po utworzeniu.

Czy te zasady działają? Co semantycznie oznacza struct?


247
System.Drawing.Rectanglenarusza wszystkie trzy z tych zasad.
ChrisW,

4
istnieje wiele komercyjnych gier napisanych w C #, chodzi o to, że są one wykorzystywane do zoptymalizowania kodu
BlackTigerX

25
Struktury zapewniają lepszą wydajność, gdy masz małe kolekcje typów wartości, które chcesz zgrupować razem. Dzieje się tak przez cały czas w programowaniu gier, na przykład wierzchołek w modelu 3D będzie miał pozycję, współrzędną tekstury i normalną, i na ogół będzie niezmienny. Pojedynczy model może mieć kilka tysięcy wierzchołków lub może mieć kilkanaście, ale struktury zapewniają mniej kosztów ogólnych w tym scenariuszu użytkowania. Zweryfikowałem to na podstawie własnego projektu silnika.
Chris D.


4
@ChrisW Rozumiem, ale czy te wartości nie reprezentują prostokąta, który jest wartością „pojedynczą”? Podobnie jak Vector3D lub Kolor, w środku jest też kilka wartości, ale myślę, że reprezentują pojedyncze wartości?
Marson Mao

Odpowiedzi:


604

Ź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:

  1. Logicznie reprezentuje pojedynczą wartość, podobną do typów pierwotnych (liczba całkowita, podwójna itd.).
  2. Ma rozmiar instancji mniejszy niż 16 bajtów.
  3. Jest niezmienny.
  4. 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:

  1. Każda struktura Entryi Enumeratorreprezentuje pojedyncze wartości.
  2. Prędkość
  3. Entrynigdy nie jest przekazywany jako parametr poza klasą Dictionary. Dalsze dochodzenie pokazuje, że aby spełnić implementację IEnumerable, Dictionary używa Enumeratorstruktury, którą kopiuje za każdym razem, gdy żądany jest moduł wyliczający ... ma sens.
  4. Wewnętrzny do klasy Dictionary. Enumeratorjest 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:

  1. Nic w elemencie zadeklarowanych powyżej readonly- nie niezmienna
  2. Rozmiar tych struktur może wynosić znacznie ponad 16 bajtów
  3. Entryma 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 OnResizewydarzenia, 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 Entrybył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ż Entryjest strukturą, nie wymaga inicjalizacji jak typ odwołania. Jest to zarówno piękno, jak i zmora typu wartości. Aby użyć Entryjako 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 Entryjako 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 :

  1. Struktura musi być bezpieczna w użyciu
  2. Struktur musi skutecznie wykonywać swoją funkcję, chyba że naruszałoby to zasadę nr 1
  3. 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).


8
Jeśli chodzi o reguły Microsoftu, wydaje się, że zasada niezmienności ma na celu zniechęcenie do używania typów wartości w taki sposób, że ich zachowanie różni się od zachowania typów referencyjnych, niezależnie od tego, że semantyka wartości częściowych może być przydatna . Jeśli posiadanie typu może podlegać częściowej mutacji, łatwiej byłoby z nim pracować, a jeśli miejsca przechowywania tego typu powinny być logicznie od siebie oddzielone, typ powinien być strukturą „mutable”.
supercat


2
Fakt, że wiele typów Microsoft narusza te reguły, nie stanowi problemu z tymi typami, ale raczej wskazuje, że reguły nie powinny mieć zastosowania do wszystkich typów struktur. Jeśli struktura reprezentuje pojedynczy byt [jak z Decimallub DateTime], to jeśli nie przestrzegałaby pozostałych trzech reguł, powinna zostać zastąpiona klasą. Jeśli struktura zawiera ustaloną kolekcję zmiennych, z których każda może zawierać dowolną wartość, która byłaby poprawna dla jej typu [np. Rectangle], Wówczas powinna przestrzegać innych reguł, z których niektóre są sprzeczne z tymi dla struktur „pojedynczej wartości” .
supercat

4
@IAbstract: Niektóre osoby uzasadniają Dictionarytyp wpisu na podstawie tego, że jest to typ wewnętrzny, wydajność uznawano za ważniejszą od semantyki lub inną wymówkę. Chodzi mi o to, że typ typu Rectanglepowinien mieć swoją zawartość ujawnioną jako pola edytowalne indywidualnie, nie „ponieważ” korzyści wynikające z wydajności przeważają nad wynikowymi niedoskonałościami semantycznymi, ale ponieważ typ semantycznie reprezentuje stały zestaw niezależnych wartości , a zatem zmienna struktura jest zarówno bardziej wydajny i semantycznie lepszy .
supercat

2
@ supercat: Zgadzam się ... i cała moja odpowiedź brzmiała, że ​​„wytyczne” są dość słabe i że struktur należy używać z pełną wiedzą i zrozumieniem zachowań. Zobacz moją odpowiedź na temat zmiennej struktury tutaj: stackoverflow.com/questions/8108920/...
IAbstract

154

Ilekroć nie potrzebujesz polimorfizmu, potrzebujesz semantyki wartości i chcesz uniknąć przydziału sterty i związanego z nią narzutu śmieci. Zastrzeżeniem jest jednak to, że struktury (dowolnie duże) są droższe do przekazania niż odwołania do klas (zwykle jedno słowo maszynowe), więc w praktyce klasy mogą być szybsze.


1
To tylko jedno „zastrzeżenie”. Należy również rozważyć „podniesienie” typów wartości i przypadków, takich jak (Guid)null(można rzucić zero na typ odniesienia), między innymi.

1
droższy niż w C / C ++? w C ++ zalecanym sposobem jest przekazywanie obiektów według wartości
Ion Todirel

@IonTodirel Czy to nie ze względów bezpieczeństwa pamięci, a nie wydajności? Zawsze jest to kompromis, ale przekazywanie 32 B przez stos zawsze jest (TM) wolniejsze niż przekazywanie odniesienia 4 B przez rejestr. Pamiętaj jednak , że użycie „wartość / referencja” jest nieco inne w C # i C ++ - kiedy przekazujesz referencję do obiektu, wciąż przekazujesz wartość, nawet jeśli przekazujesz referencję („ przekazywanie wartości referencji, a nie referencji, w zasadzie). To nie jest semantyka wartości , ale technicznie „pass-by-value”.
Luaan,

@Luaan Kopiowanie to tylko jeden aspekt kosztów. Dodatkowa pośrednia konsekwencja ze względu na wskaźnik / odniesienie również koszty na dostęp. W niektórych przypadkach strukturę można nawet przenieść, a zatem nie trzeba jej nawet kopiować.
Onur

@Onur to ciekawe. Jak się „poruszasz” bez kopiowania? Myślałem, że instrukcja asm „mov” tak naprawdę nie „porusza się”. Kopiuje.
Winger Sendon

148

Nie zgadzam się z zasadami podanymi w oryginalnym poście. Oto moje zasady:

1) Używasz struktur do wydajności, gdy są przechowywane w tablicach. (patrz także: Kiedy struktury są odpowiedzią? )

2) Potrzebujesz ich w kodzie przekazującym dane strukturalne do / z C / C ++

3) Nie używaj struktur, chyba że są potrzebne:

  • Zachowują się inaczej niż „normalne obiekty” ( typy referencyjne ) podczas przypisywania i przekazywania jako argumenty, co może prowadzić do nieoczekiwanego zachowania; jest to szczególnie niebezpieczne, jeśli osoba oglądająca kod nie wie, że ma do czynienia z strukturą.
  • Nie można ich odziedziczyć.
  • Przekazywanie struktur jako argumentów jest droższe niż klasy.

4
+1 Tak, zgadzam się całkowicie na nr 1 (jest to ogromna zaleta przy radzeniu sobie z takimi rzeczami, jak obrazy, itp.) Oraz za wskazanie, że różnią się one od „normalnych obiektów” i istnieje znany sposób na to, z wyjątkiem istniejącej wiedzy lub badanie samego typu. Ponadto, nie możesz rzucić wartości null na typ struktury :-) Jest to właściwie jeden przypadek, w którym prawie żałuję, że nie było trochę „węgierskiego” dla typów wartości innych niż Core lub obowiązkowego słowa kluczowego „struct” w witrynie deklaracji zmiennych .

@pst: Prawdą jest, że trzeba wiedzieć, że coś ma structwiedzieć, jak się zachowa, ale jeśli coś jest structz odkrytymi polami, to wszystko, co musisz wiedzieć. Jeśli obiekt ujawnia właściwość typu narażonego pola-struktury, a jeśli kod odczytuje tę strukturę do zmiennej i modyfikuje, można bezpiecznie przewidzieć, że takie działanie nie wpłynie na obiekt, którego właściwość została odczytana, chyba że lub do momentu napisania struktury plecy. Natomiast, jeżeli nieruchomość była zmienny typ klasy, czyta je i modyfikując może zaktualizować podstawowy obiekt zgodnie z oczekiwaniami, ale ...
Supercat

... może również w końcu nic nie zmienić, lub może zmienić lub uszkodzić przedmioty, których nie zamierzamy zmieniać. Kod, którego semantyka mówi „zmień tę zmienną tak, jak lubisz; zmiany nic nie zrobią, dopóki nie zapiszesz jej gdzieś” wydaje się wyraźniejszy niż kod, który mówi: „Otrzymujesz odwołanie do jakiegoś obiektu, który może być współdzielony z dowolną liczbą innych odniesień lub w ogóle mogą nie być udostępniane; musisz dowiedzieć się, kto jeszcze może mieć odniesienia do tego obiektu, aby wiedzieć, co się stanie, jeśli go zmienisz ”.
supercat

Nakładaj na # 1. Lista pełna struktur może wcisnąć znacznie bardziej odpowiednie dane do pamięci podręcznej L1 / L2 niż lista pełna odwołań do obiektów (dla struktury o odpowiednim rozmiarze).
Matt Stephenson,

2
Dziedziczenie rzadko jest właściwym narzędziem do pracy, a zbytnie rozumowanie wydajności bez profilowania jest złym pomysłem. Po pierwsze, struktury mogą być przekazywane przez referencję. Po drugie, przekazywanie przez odniesienie lub wartość rzadko stanowi istotny problem z wydajnością. Na koniec nie bierzesz pod uwagę dodatkowego przydziału sterty i wyrzucania elementów bezużytecznych dla klasy. Osobiście wolę myśleć o strukturach jako zwykłych danych i klasach jako o rzeczach, które wykonują różne rzeczy (obiekty), chociaż można również definiować metody w strukturach.
weberc2

87

Użyj struktury, jeśli chcesz wartościować semantykę, a nie semantykę odniesienia.

Edytować

Nie jestem pewien, dlaczego ludzie tak głoszą głosowanie, ale jest to słuszna uwaga, która została postawiona przed op wyjaśnieniem jego pytania i jest to najbardziej podstawowy podstawowy powód struktury.

Jeśli potrzebujesz semantyki odniesienia, potrzebujesz klasy, a nie struktury.


13
Wszyscy to wiedzą. Wygląda na to, że szuka czegoś więcej niż „struct jest typem wartości”.
TheSmurf

21
Jest to najbardziej podstawowy przypadek i powinien być określony dla każdego, kto czyta ten post i nie wie o tym.
JoshBerke,

3
Nie to, że ta odpowiedź nie jest prawdziwa; to oczywiście jest. Nie o to chodzi.
TheSmurf

55
@Josh: Dla każdego, kto jeszcze tego nie wie, po prostu powiedzenie, że jest to niewystarczająca odpowiedź, ponieważ jest całkiem prawdopodobne, że nie wiedzą, co to znaczy.
TheSmurf

1
Właśnie zanegowałem to, ponieważ uważam, że jedna z pozostałych odpowiedzi powinna znajdować się na górze - każda odpowiedź, która mówi: „Aby współpracować z niezarządzanym kodem, inaczej unikaj”.
Daniel Earwicker

59

Oprócz odpowiedzi „jest to wartość”, jednym konkretnym scenariuszem korzystania ze struktur jest, gdy wiesz , że masz zestaw danych, który powoduje problemy z odśmiecaniem, i masz wiele obiektów. Na przykład duża lista / tablica instancji Person. Naturalną metaforą jest tutaj klasa, ale jeśli masz dużą liczbę długo żyjących instancji Osoby, mogą one w efekcie zapchać GEN-2 i spowodować utknięcie GC. Jeśli scenariusz to uzasadnia, jednym z potencjalnych podejść jest użycie tablicy (nie listy) struktur Person , tj Person[]. Teraz, zamiast mieć miliony obiektów w GEN-2, masz pojedynczą porcję na LOH (zakładam, że nie ma tutaj ciągów znaków itp. - tj. Czysta wartość bez żadnych odniesień). Ma to bardzo niewielki wpływ na GC.

Praca z tymi danymi jest niewygodna, ponieważ dane są prawdopodobnie zbyt duże dla struktury i nie chcesz przez cały czas kopiować grubych wartości. Jednak dostęp do niego bezpośrednio w tablicy nie kopiuje struktury - jest on na miejscu (w przeciwieństwie do indeksu listy, który kopiuje). Oznacza to dużo pracy z indeksami:

int index = ...
int id = peopleArray[index].Id;

Zauważ, że utrzymanie samych wartości niezmiennych pomoże tutaj. Aby uzyskać bardziej złożoną logikę, użyj metody z parametrem by-ref:

void Foo(ref Person person) {...}
...
Foo(ref peopleArray[index]);

Znów jest to na miejscu - nie skopiowaliśmy wartości.

W bardzo szczególnych scenariuszach ta taktyka może być bardzo skuteczna; jest to jednak dość zaawansowany scernario, którego należy próbować tylko wtedy, gdy wiesz, co robisz i dlaczego. Domyślnie tutaj byłaby klasa.


+1 Interesująca odpowiedź. Czy zechciałbyś podzielić się anegdotami ze świata rzeczywistego na temat takiego podejścia?
Jordão

@Jordao na telefonie komórkowym, ale wyszukaj w Google: + gravell + „atak przez GC”
Marc Gravell

1
Wielkie dzięki. Znalazłem to tutaj .
Jordão

2
@MarcGravell Dlaczego wspomniałeś: użyj tablicy (nie listy) ? ListWydaje mi się, że używa Arrayscen zakulisowych. nie?
Royi Namir

4
@RoyiNamir Też byłem ciekawy, ale wierzę, że odpowiedź leży w drugim akapicie odpowiedzi Marca. „Jednak dostęp do niego bezpośrednio w tablicy nie kopiuje struktury - jest on na miejscu (w przeciwieństwie do indeksu listy, który kopiuje)”.
user1323245

40

Ze specyfikacji języka C # :

1.7 Struktury

Struktury, podobnie jak klasy, są strukturami danych, które mogą zawierać elementy danych i elementy funkcji, ale w przeciwieństwie do klas, struktury są typami wartości i nie wymagają alokacji sterty. Zmienna typu struct bezpośrednio przechowuje dane struktury, podczas gdy zmienna typu klasy przechowuje odwołanie do dynamicznie przydzielanego obiektu. Typy struktur nie obsługują dziedziczenia określonego przez użytkownika, a wszystkie typy struktur domyślnie dziedziczą po obiekcie typu.

Struktury są szczególnie przydatne w przypadku małych struktur danych, które mają semantykę wartości. Liczby zespolone, punkty w układzie współrzędnych lub pary klucz-wartość w słowniku są dobrymi przykładami struktur. Użycie struktur zamiast klas w przypadku małych struktur danych może mieć dużą różnicę w liczbie przydziałów pamięci wykonywanych przez aplikację. Na przykład poniższy program tworzy i inicjuje tablicę 100 punktów. Dzięki Point zaimplementowanemu jako klasa, tworzonych jest 101 osobnych obiektów - jeden dla tablicy i jeden dla 100 elementów.

class Point
{
   public int x, y;

   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }
}

class Test
{
   static void Main() {
      Point[] points = new Point[100];
      for (int i = 0; i < 100; i++) points[i] = new Point(i, i);
   }
}

Alternatywą jest uczynienie z Point struktury.

struct Point
{
   public int x, y;

   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }
}

Teraz tworzony jest tylko jeden obiekt - ten dla tablicy - a instancje Point są przechowywane w tablicy w linii.

Konstruktory struktur są wywoływane z nowym operatorem, ale nie oznacza to, że pamięć jest przydzielana. Zamiast dynamicznie alokować obiekt i zwracać do niego odwołanie, konstruktor struktury po prostu zwraca samą wartość struktury (zwykle w tymczasowej lokalizacji na stosie), a następnie ta wartość jest następnie kopiowana w razie potrzeby.

W przypadku klas dwie zmienne mogą odwoływać się do tego samego obiektu, a zatem operacje na jednej zmiennej mogą wpływać na obiekt, do którego odwołuje się druga zmienna. W przypadku struktur każda zmienna ma własną kopię danych i operacje na jednym z nich nie mają wpływu na drugi. Na przykład wynik generowany przez następujący fragment kodu zależy od tego, czy Point jest klasą, czy strukturą.

Point a = new Point(10, 10);
Point b = a;
a.x = 20;
Console.WriteLine(b.x);

Jeśli Point jest klasą, wynikiem jest 20, ponieważ aib odnoszą się do tego samego obiektu. Jeśli Point jest strukturą, wynikiem jest 10, ponieważ przypisanie a do b tworzy kopię wartości, a na tę kopię nie ma wpływu późniejsze przypisanie do ax

Poprzedni przykład podkreśla dwa ograniczenia struktur. Po pierwsze, kopiowanie całej struktury jest zwykle mniej wydajne niż kopiowanie odwołania do obiektu, więc przypisanie i przekazanie parametrów wartości może być droższe w przypadku struktur niż w przypadku typów odniesienia. Po drugie, z wyjątkiem parametrów ref i out, nie można tworzyć odwołań do struktur, co wyklucza ich użycie w wielu sytuacjach.


4
Chociaż fakt, że nie można utrwalić odniesień do struktur, jest czasem ograniczeniem, jest również bardzo przydatną cechą. Jedną z głównych słabości .net jest to, że nie ma przyzwoitego sposobu na przekazanie poza kod odwołania do obiektu zmiennego bez wiecznej utraty kontroli nad tym obiektem. W przeciwieństwie do tego, można bezpiecznie podać metodę zewnętrzną refmutowalnej strukturze i wiedzieć, że wszelkie mutacje, które wykona na niej metoda zewnętrzna, zostaną wykonane przed jej powrotem. Szkoda, że ​​.net nie ma pojęcia o efemerycznych parametrach i zwracanych wartościach funkcji, ponieważ ...
supercat

4
... to pozwoliłoby uzyskać korzystną semantykę przekazywanych struktur refza pomocą obiektów klasy. Zasadniczo zmienne lokalne, parametry i zwracane wartości funkcji mogą być trwałe (domyślne), zwrotne lub efemeryczne. Kod miałby zakaz kopiowania efemerycznych rzeczy do czegokolwiek, co wykraczałoby poza obecny zakres. Rzeczy zwrotne byłyby niczym efemeryczne, z wyjątkiem tego, że mogłyby zostać zwrócone z funkcji. Zwracana wartość funkcji byłaby ograniczona najostrzejszymi ograniczeniami mającymi zastosowanie do któregokolwiek z jej parametrów „zwrotnych”.
supercat

34

Struktury są dobre do atomowej reprezentacji danych, przy czym dane te mogą być wielokrotnie kopiowane przez kod. Klonowanie obiektu jest na ogół droższe niż kopiowanie struktury, ponieważ wymaga alokacji pamięci, uruchomienia konstruktora i dezalokacji / wyrzucania elementów bezużytecznych.


4
Tak, ale duże struktury mogą być droższe niż odwołania do klas (przy przechodzeniu do metod).
Alex

27

Oto podstawowa zasada.

  • Jeśli wszystkie pola składowe są typami wartości, utwórz strukturę .

  • Jeśli jedno pole członka jest typem odniesienia, utwórz klasę . Jest tak, ponieważ pole typu odwołania i tak będzie wymagało przydziału sterty.

Przykłady

public struct MyPoint 
{
    public int X; // Value Type
    public int Y; // Value Type
}

public class MyPointWithName 
{
    public int X; // Value Type
    public int Y; // Value Type
    public string Name; // Reference Type
}

3
Niezmienne typy referencyjne, takie jak, stringsą semantycznie równoważne wartościom, a przechowywanie referencji do niezmiennego obiektu w polu nie pociąga za sobą przydziału sterty. Różnica między strukturą z odsłoniętymi polami publicznymi a obiektem klasy z odsłoniętymi polami publicznymi polega na tym, że biorąc pod uwagę sekwencję kodu var q=p; p.X=4; q.X=5;, p.Xbędzie miała wartość 4, jeśli ajest typem struktury, i 5, jeśli jest to typ klasy. Jeśli ktoś chce mieć możliwość wygodnej modyfikacji elementów tego typu, powinien wybrać „klasę” lub „strukturę” w oparciu o to, czy chce qwpłynąć na zmiany p.
supercat

Tak, zgadzam się, że zmienna referencyjna będzie na stosie, ale obiekt, do którego się odwołuje, będzie istniał na stercie. Chociaż struktury i klasy zachowują się inaczej, gdy są przypisane do innej zmiennej, ale nie sądzę, że jest to silny czynnik decydujący.
Usman Zafar

Zmienne struktury i klasy zmiennych zachowują się zupełnie inaczej; jeśli jeden ma rację, drugi najprawdopodobniej się myli. Nie jestem pewien, w jaki sposób zachowanie nie będzie decydującym czynnikiem przy określaniu, czy należy użyć struktury, czy klasy.
supercat

Powiedziałem, że nie jest to silny czynnik decydujący, ponieważ często, kiedy tworzysz klasę lub strukturę, nie jesteś pewien, jak zostanie ona wykorzystana. Koncentrujesz się na tym, jak wszystko ma sens z punktu widzenia projektowania. W każdym razie nigdy nie widziałem w jednym miejscu w bibliotece .NET, gdzie struct zawiera zmienną referencyjną.
Usman Zafar

1
Typ struktury ArraySegment<T>zawiera w sobie typ T[], który zawsze jest typem klasy. Typ struktury KeyValuePair<TKey,TValue>jest często używany z typami klas jako parametry ogólne.
supercat

19

Po pierwsze: scenariusze interakcji lub gdy musisz określić układ pamięci

Po drugie: kiedy dane mają prawie taki sam rozmiar jak wskaźnik odniesienia.


17

Musisz użyć „struct” w sytuacjach, w których chcesz jawnie określić układ pamięci za pomocą StructLayoutAttribute - zazwyczaj dla PInvoke.

Edycja: Komentarz wskazuje, że możesz używać klasy lub struktury za pomocą StructLayoutAttribute i to z pewnością prawda. W praktyce zwykle używasz struktury - jest ona przypisywana do stosu w stosunku do stosu, co ma sens, jeśli przekazujesz argument do niezarządzanego wywołania metody.


5
StructLayoutAttribute można zastosować do struktur lub klas, więc nie jest to powód do używania struktur.
Stephen Martin

Dlaczego ma to sens, jeśli przekazujesz argument do niezarządzanego wywołania metody?
David Klempfner

16

Używam struktur do pakowania lub rozpakowywania dowolnego binarnego formatu komunikacji. Obejmuje to odczyt lub zapis na dysk, listy wierzchołków DirectX, protokoły sieciowe lub postępowanie z zaszyfrowanymi / skompresowanymi danymi.

Trzy wymienione przez ciebie wytyczne nie były dla mnie przydatne w tym kontekście. Kiedy muszę wypisać czterysta bajtów rzeczy w określonym porządku, zdefiniuję czterysta bajtową strukturę i wypełnię ją wszelkimi niepowiązanymi wartościami, które powinien mieć, i idę ustawić w dowolny sposób, jaki jest najbardziej sensowny w danym momencie. (Okej, czterysta bajtów byłoby dość dziwnych - ale kiedy pisałem do życia pliki Excela, miałem do czynienia ze strukturami do około czterdziestu bajtów, ponieważ tak duże są niektóre rekordy BIFF.)


Czy nie możesz równie łatwo użyć do tego celu typu referencyjnego?
David Klempfner

15

Z wyjątkiem typów wartości, które są używane bezpośrednio przez środowisko wykonawcze i różnych innych do celów PInvoke, należy używać typów wartości tylko w 2 scenariuszach.

  1. Kiedy potrzebujesz skopiować semantykę.
  2. Gdy potrzebujesz automatycznej inicjalizacji, zwykle w tablicach tego typu.

# 2 wydaje się być jednym z powodów rozpowszechnienia struktury w klasach kolekcji .Net.
IAbstract

Jeśli pierwszą rzeczą, którą należy zrobić po utworzeniu miejsca przechowywania typu klasy, byłoby utworzenie nowej instancji tego typu, zapisz odniesienie do niego w tym miejscu i nigdy nie kopiuj go nigdzie indziej, ani nie nadpisuj, a następnie a klasa zachowałaby się identycznie. Struktury mają wygodny standardowy sposób kopiowania wszystkich pól z jednej instancji do drugiej i generalnie oferują lepszą wydajność w przypadkach, w których nigdy nie powielono by odwołania do klasy (z wyjątkiem efemerycznego thisparametru używanego do wywoływania jego metod); klasy pozwalają na powielanie referencji.
supercat

13

.NET obsługuje value typesi reference types(w Javie można zdefiniować tylko typy referencyjne). Wystąpienia reference typesget przydzielane są na zarządzanej stercie i są usuwane, gdy nie ma zaległych odwołań do nich. value typesZ drugiej strony instancje są alokowane w stackpamięci, a zatem przydzielona pamięć jest odzyskiwana, gdy tylko kończy się ich zakres. I oczywiście value typesmijamy według wartości i reference typesreferencji. Wszystkie pierwotne typy danych C #, z wyjątkiem System.String, są typami wartości.

Kiedy używać struct ponad klasą,

W C # structsvalue typesklasy reference types. Możesz tworzyć typy wartości w C #, używając enumsłowa kluczowego i structsłowa kluczowego. Użycie value typezamiast zamiast reference typespowoduje zmniejszenie liczby obiektów na zarządzanej stercie, co spowoduje mniejsze obciążenie modułu wyrzucania elementów bezużytecznych (GC), rzadsze cykle GC, aw konsekwencji lepszą wydajność. Miej jednak value typesrównież swoje wady. Przekazanie dużego structjest zdecydowanie droższe niż przekazanie referencji, to jeden oczywisty problem. Innym problemem jest narzut związany z boxing/unboxing. Jeśli zastanawiasz się, co to boxing/unboxingznaczy, skorzystaj z tych linków, aby uzyskać dobre wyjaśnienie na tematboxing iunboxing. Oprócz wydajności zdarzają się sytuacje, gdy po prostu potrzebujesz typów, aby mieć semantykę wartości, co byłoby bardzo trudne (lub brzydkie) do wdrożenia, jeśli reference typeswszystko, co masz. Powinieneś używać value typestylko, gdy potrzebujesz skopiować semantykę lub potrzebujesz automatycznej inicjalizacji, zwykle w arraystych typach.


Kopiowanie małych struktur lub przekazywanie wartości jest tak tanie, jak kopiowanie lub przekazywanie referencji klasowych lub przekazywanie struktur ref. Przechodząc przez dowolną strukturę wielkościref kosztami jest takie samo jak przekazywanie referencji klasy pod względem wartości. Kopiowanie dowolnej struktury wielkości lub przekazywanie według wartości jest tańsze niż wykonywanie kopii obronnej obiektu klasy i przechowywanie lub przekazywanie odniesienia do tego. Wielkie czasy klasy są lepsze niż struktury do przechowywania wartości (1), gdy klasy są niezmienne (aby uniknąć kopiowania obronnego), a każda utworzona instancja będzie dużo przekazywana, lub ...
supercat

... (2) gdy z różnych powodów struktura po prostu nie byłaby użyteczna [np. Ponieważ trzeba użyć referencji zagnieżdżonych dla czegoś takiego jak drzewo lub ponieważ potrzebujemy polimorfizmu]. Zauważ, że podczas korzystania z typów wartości, na ogół należy odsłonić pola bezpośrednio bez konkretnego powodu, aby tego nie robić (podczas gdy w przypadku większości typów klas pola powinny być zawinięte we właściwościach). Wiele z tak zwanych „zła” zmiennych typów wartości wynika z niepotrzebnego zawijania pól we właściwościach (np. Podczas gdy niektóre kompilatory pozwalają wywoływać funkcję ustawiającą właściwości w strukturze tylko do odczytu, ponieważ czasami ...
supercat

... postępuj właściwie, wszystkie kompilatory właściwie odrzucałyby próby bezpośredniego ustawienia pól na takich strukturach; najlepszym sposobem na upewnienie się, że kompilatory odrzucają, readOnlyStruct.someMember = 5;nie jest utworzenie someMemberwłaściwości tylko do odczytu, ale zamiast tego uczynienie z niej pola.
supercat

12

Struct jest typ wartości. Jeśli przypiszesz strukturę do nowej zmiennej, nowa zmienna będzie zawierać kopię oryginału.

public struct IntStruct {
    public int Value {get; set;}
}

Wykonanie następujących wyników powoduje 5 wystąpień struktury przechowywanej w pamięci:

var struct1 = new IntStruct() { Value = 0 }; // original
var struct2 = struct1;  // A copy is made
var struct3 = struct2;  // A copy is made
var struct4 = struct3;  // A copy is made
var struct5 = struct4;  // A copy is made

// NOTE: A "copy" will occur when you pass a struct into a method parameter.
// To avoid the "copy", use the ref keyword.

// Although structs are designed to use less system resources
// than classes.  If used incorrectly, they could use significantly more.

Klasa jest typem odniesienia. Gdy przypisujesz klasę do nowej zmiennej, zmienna zawiera odwołanie do oryginalnego obiektu klasy.

public class IntClass {
    public int Value {get; set;}
}

Wykonanie poniższych wyników powoduje tylko jedno wystąpienie obiektu klasy w pamięci.

var class1 = new IntClass() { Value = 0 };
var class2 = class1;  // A reference is made to class1
var class3 = class2;  // A reference is made to class1
var class4 = class3;  // A reference is made to class1
var class5 = class4;  // A reference is made to class1  

Struct s mogą zwiększać prawdopodobieństwo błędu kodu. Jeśli obiekt wartości jest traktowany jak zmienny obiekt odniesienia, programista może być zaskoczony, gdy wprowadzone zmiany zostaną nieoczekiwanie utracone.

var struct1 = new IntStruct() { Value = 0 };
var struct2 = struct1;
struct2.Value = 1;
// At this point, a developer may be surprised when 
// struct1.Value is 0 and not 1

12

Zrobiłem mały test porównawczy z BenchmarkDotNet aby lepiej zrozumieć korzyści „strukturalne” w liczbach. Testuję zapętlanie tablicy (lub listy) struktur (lub klas). Tworzenie tych tablic lub list nie wchodzi w zakres testu porównawczego - jasne jest, że „klasa” jest bardziej obciążona, zużyje więcej pamięci i będzie wymagała GC.

Wniosek jest następujący: bądź ostrożny z LINQ i ukrytymi strukturami do boksowania / rozpakowywania i używanie struktur do mikrooptymalizacji ściśle trzymaj się tablic.

PS Kolejnym punktem odniesienia dla przekazywania struktury / klasy przez stos wywołań jest https://stackoverflow.com/a/47864451/506147

BenchmarkDotNet=v0.10.8, OS=Windows 10 Redstone 2 (10.0.15063)
Processor=Intel Core i5-2500K CPU 3.30GHz (Sandy Bridge), ProcessorCount=4
Frequency=3233542 Hz, Resolution=309.2584 ns, Timer=TSC
  [Host] : Clr 4.0.30319.42000, 64bit RyuJIT-v4.7.2101.1
  Clr    : Clr 4.0.30319.42000, 64bit RyuJIT-v4.7.2101.1
  Core   : .NET Core 4.6.25211.01, 64bit RyuJIT


          Method |  Job | Runtime |      Mean |     Error |    StdDev |       Min |       Max |    Median | Rank |  Gen 0 | Allocated |
---------------- |----- |-------- |----------:|----------:|----------:|----------:|----------:|----------:|-----:|-------:|----------:|
   TestListClass |  Clr |     Clr |  5.599 us | 0.0408 us | 0.0382 us |  5.561 us |  5.689 us |  5.583 us |    3 |      - |       0 B |
  TestArrayClass |  Clr |     Clr |  2.024 us | 0.0102 us | 0.0096 us |  2.011 us |  2.043 us |  2.022 us |    2 |      - |       0 B |
  TestListStruct |  Clr |     Clr |  8.427 us | 0.1983 us | 0.2204 us |  8.101 us |  9.007 us |  8.374 us |    5 |      - |       0 B |
 TestArrayStruct |  Clr |     Clr |  1.539 us | 0.0295 us | 0.0276 us |  1.502 us |  1.577 us |  1.537 us |    1 |      - |       0 B |
   TestLinqClass |  Clr |     Clr | 13.117 us | 0.1007 us | 0.0892 us | 13.007 us | 13.301 us | 13.089 us |    7 | 0.0153 |      80 B |
  TestLinqStruct |  Clr |     Clr | 28.676 us | 0.1837 us | 0.1534 us | 28.441 us | 28.957 us | 28.660 us |    9 |      - |      96 B |
   TestListClass | Core |    Core |  5.747 us | 0.1147 us | 0.1275 us |  5.567 us |  5.945 us |  5.756 us |    4 |      - |       0 B |
  TestArrayClass | Core |    Core |  2.023 us | 0.0299 us | 0.0279 us |  1.990 us |  2.069 us |  2.013 us |    2 |      - |       0 B |
  TestListStruct | Core |    Core |  8.753 us | 0.1659 us | 0.1910 us |  8.498 us |  9.110 us |  8.670 us |    6 |      - |       0 B |
 TestArrayStruct | Core |    Core |  1.552 us | 0.0307 us | 0.0377 us |  1.496 us |  1.618 us |  1.552 us |    1 |      - |       0 B |
   TestLinqClass | Core |    Core | 14.286 us | 0.2430 us | 0.2273 us | 13.956 us | 14.678 us | 14.313 us |    8 | 0.0153 |      72 B |
  TestLinqStruct | Core |    Core | 30.121 us | 0.5941 us | 0.5835 us | 28.928 us | 30.909 us | 30.153 us |   10 |      - |      88 B |

Kod:

[RankColumn, MinColumn, MaxColumn, StdDevColumn, MedianColumn]
    [ClrJob, CoreJob]
    [HtmlExporter, MarkdownExporter]
    [MemoryDiagnoser]
    public class BenchmarkRef
    {
        public class C1
        {
            public string Text1;
            public string Text2;
            public string Text3;
        }

        public struct S1
        {
            public string Text1;
            public string Text2;
            public string Text3;
        }

        List<C1> testListClass = new List<C1>();
        List<S1> testListStruct = new List<S1>();
        C1[] testArrayClass;
        S1[] testArrayStruct;
        public BenchmarkRef()
        {
            for(int i=0;i<1000;i++)
            {
                testListClass.Add(new C1  { Text1= i.ToString(), Text2=null, Text3= i.ToString() });
                testListStruct.Add(new S1 { Text1 = i.ToString(), Text2 = null, Text3 = i.ToString() });
            }
            testArrayClass = testListClass.ToArray();
            testArrayStruct = testListStruct.ToArray();
        }

        [Benchmark]
        public int TestListClass()
        {
            var x = 0;
            foreach(var i in testListClass)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestArrayClass()
        {
            var x = 0;
            foreach (var i in testArrayClass)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestListStruct()
        {
            var x = 0;
            foreach (var i in testListStruct)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestArrayStruct()
        {
            var x = 0;
            foreach (var i in testArrayStruct)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestLinqClass()
        {
            var x = testListClass.Select(i=> i.Text1.Length + i.Text3.Length).Sum();
            return x;
        }

        [Benchmark]
        public int TestLinqStruct()
        {
            var x = testListStruct.Select(i => i.Text1.Length + i.Text3.Length).Sum();
            return x;
        }
    }

Czy zorientowałeś się, dlaczego struktury są o wiele wolniejsze, gdy są używane na listach i tym podobne? Czy to z powodu ukrytego boksowania i rozpakowywania, o którym wspominałeś? Jeśli tak, dlaczego tak się dzieje?
Marko Grdinic,

Dostęp do struktury w tablicy powinien być szybszy tylko dlatego, że nie wymaga żadnych dodatkowych odwołań. Boks / Unboxing dotyczy Linq.
Roman Pokrovskij

10

Typy struktur w języku C # lub innych językach .net są zwykle używane do przechowywania rzeczy, które powinny zachowywać się jak grupy wartości o stałej wielkości. Przydatnym aspektem typów struktur jest to, że pola instancji typu struktury można modyfikować, modyfikując miejsce przechowywania, w którym są przechowywane, i w żaden inny sposób. Możliwe jest kodowanie struktury w taki sposób, że jedynym sposobem na zmutowanie dowolnego pola jest skonstruowanie zupełnie nowej instancji, a następnie użycie przypisania struktury do mutacji wszystkich pól obiektu docelowego poprzez zastąpienie ich wartościami z nowej instancji, ale chyba że struct nie daje możliwości utworzenia instancji, w której jego pola mają wartości inne niż domyślne, wszystkie jego pola będą podlegały mutacji, jeśli i jeśli sama struktura jest przechowywana w zmiennej lokalizacji.

Zauważ, że możliwe jest zaprojektowanie typu struktury tak, aby zasadniczo zachowywał się jak typ klasy, jeśli struktura zawiera prywatne pole typu klasy i przekierowuje własne elementy do owiniętego obiektu klasy. Na przykład, PersonCollectionwłaściwości oferują moc SortedByNamei SortedById, z których oba przytrzymaj „niezmienny” odniesienie do PersonCollection(ustalony w ich konstruktora) i wdrożyć GetEnumeratordzwoniąc albo creator.GetNameSortedEnumeratoralbo creator.GetIdSortedEnumerator. Takie struktury zachowywałyby się jak odniesienia do a PersonCollection, z tym wyjątkiem, że ich GetEnumeratormetody byłyby powiązane z różnymi metodami w PersonCollection. Można również mieć strukturę owijającą część tablicy (np. Można zdefiniować ArrayRange<T>strukturę, która będzie zawierać T[]wywołane Arr, int Offseti intLength, z indeksowaną właściwością, do której dostęp miałby dostęp dla indeksu idxz zakresu od 0 do ). Niestety, jeśli taka struktura jest przeznaczona tylko do odczytu, bieżące wersje kompilatora nie zezwalają na takie operacje, ponieważ nie mają możliwości ustalenia, czy takie operacje będą próbowały zapisać w polach .Length-1Arr[idx+Offset]foofoo[3]+=4;foo

Możliwe jest również zaprojektowanie struktury tak, aby zachowywała się jak typ wartości, który zawiera kolekcję o zmiennej wielkości (która będzie wyglądać, jakby była kopiowana za każdym razem, gdy jest w strukturze), ale jedynym sposobem na wykonanie tej pracy jest upewnienie się, że żaden obiekt, do którego struct posiada odnośnik, który będzie kiedykolwiek wystawiony na wszystko, co może go zmutować. Na przykład, można mieć strukturę tablicową, która przechowuje tablicę prywatną i której zindeksowana metoda „put” tworzy nową tablicę, której zawartość jest podobna do oryginalnej z wyjątkiem jednego zmienionego elementu. Niestety sprawienie, by takie struktury działały wydajnie, może być nieco trudne. Chociaż zdarza się, że semantyka struct może być wygodna (np. Możliwość przekazania kolekcji podobnej do tablicy do procedury, wywołujący i odbierający wiedzą, że zewnętrzny kod nie zmodyfikuje kolekcji,


10

Nie - nie do końca zgadzam się z zasadami. Są to dobre wytyczne do rozważenia przy wydajności i standaryzacji, ale nie w świetle możliwości.

Jak widać w odpowiedziach, istnieje wiele kreatywnych sposobów ich wykorzystania. Dlatego te wytyczne muszą być takie, zawsze ze względu na wydajność i wydajność.

W tym przypadku używam klas do reprezentowania obiektów świata rzeczywistego w ich większej formie, używam struktur do reprezentowania mniejszych obiektów, które mają bardziej dokładne zastosowania. Sposób, w jaki to powiedziałeś, „bardziej spójna całość”. Słowo kluczowe jest spójne. Klasy będą bardziej obiektowymi elementami, podczas gdy struktury mogą mieć niektóre z tych cech, choć w mniejszej skali. IMO.

Używam ich często w znacznikach Treeview i Listview, gdzie bardzo szybko można uzyskać dostęp do wspólnych atrybutów statycznych. Zawsze starałem się uzyskać te informacje w inny sposób. Na przykład w moich aplikacjach bazodanowych korzystam z widoku drzewa, w którym mam tabele, SP, funkcje lub dowolne inne obiekty. Tworzę i wypełniam moją strukturę, umieszczam ją w tagu, wyciągam, pobieram dane zaznaczenia i tak dalej. Nie zrobiłbym tego z klasą!

Staram się, aby były małe, używam ich w sytuacjach pojedynczych i powstrzymuję je przed zmianą. Rozsądnie jest pamiętać o pamięci, przydziale i wydajności. Testowanie jest tak konieczne.


Struktury mogą być rozsądnie użyte do przedstawienia lekkich, niezmiennych obiektów, lub mogą być sensownie użyte do przedstawienia stałych zbiorów powiązanych, ale niezależnych zmiennych (np. Współrzędne punktu). Porady na tej stronie są dobre dla struktur, które mają służyć pierwszemu celowi, ale są złe dla struktur, które mają służyć drugiemu celowi. Moje obecne myślenie jest takie, że struktury, które mają dowolne pola prywatne, powinny ogólnie spełniać wskazany opis, ale wiele struktur powinno ujawniać cały swój stan poprzez pola publiczne.
supercat

Jeśli specyfikacja typu „punktu 3d” wskazuje, że cały jego stan jest ujawniany przez czytelne elementy x, y i z, i możliwe jest utworzenie instancji z dowolną kombinacją doublewartości dla tych współrzędnych, taka specyfikacja zmusiłaby go do zachowują się semantycznie identycznie do struktury pola narażonego, z wyjątkiem niektórych szczegółów zachowania wielowątkowego (klasa niezmienna byłaby lepsza w niektórych przypadkach, podczas gdy struktura pola narażonego byłaby lepsza w innych; tak zwana „struktura niezmienna” byłaby gorsze w każdym przypadku).
supercat

8

Moją zasadą jest

1, Zawsze używaj klasy;

2, jeśli występuje jakikolwiek problem z wydajnością, próbuję zmienić klasę na struct w zależności od reguł wymienionych przez @IAbstract, a następnie wykonuję test, aby sprawdzić, czy zmiany te mogą poprawić wydajność.


Istotnym przypadkiem użycia, który Microsoft ignoruje, jest sytuacja, gdy chce się, aby zmienna typu Foozawierała stały zbiór niezależnych wartości (np. Współrzędnych punktu), które czasem chce się przekazywać jako grupa, a czasem chce się zmieniać niezależnie. Nie znalazłem żadnego wzorca używania klas, który łączy oba cele prawie tak ładnie, jak zwykła struktura pola narażonego (która, jako stały zbiór niezależnych zmiennych, idealnie pasuje do rachunku).
supercat

1
@ superupat: Myślę, że obwinianie za to Microsoft nie jest do końca sprawiedliwe. Prawdziwy problem polega na tym, że C # jako język obiektowy po prostu nie koncentruje się na zwykłych typach rekordów, które ujawniają dane bez większego zachowania. C # nie jest językiem paradygmatu w takim samym stopniu, jak np. C ++. Biorąc to pod uwagę, uważam również, że niewielu ludzi programuje czysty OOP, więc być może C # jest zbyt idealistycznym językiem. (Z jednej strony ostatnio zacząłem również ujawniać public readonlypola w moich typach, ponieważ tworzenie właściwości tylko do odczytu to po prostu zbyt dużo pracy, praktycznie bez żadnych korzyści.)
stakx - już nie wnosi

1
@stakx: Nie ma potrzeby, aby „skupiał się” na takich typach; rozpoznanie ich takimi, jakimi są, wystarczyłoby. Największą słabością C # w odniesieniu do struktur jest również największy problem w wielu innych obszarach: język zapewnia nieodpowiednie możliwości wskazywania, kiedy pewne transformacje są lub nie są odpowiednie, a brak takich udogodnień powoduje niefortunne decyzje projektowe. Na przykład 99% „zmiennych struktur jest złych” wynika z przekształcenia MyListOfPoint[3].Offset(2,3);się kompilatora w var temp=MyListOfPoint[3]; temp.Offset(2,3);transformację, która jest nieprawdziwa po zastosowaniu ...
supercat

... do Offsetmetody. Właściwym sposobem zapobiegania takiemu fałszywemu kodowi nie powinno być uczynienie struktur niepotrzebnie niezmiennymi, lecz umożliwienie metodom takim jak Offsetoznaczanie atrybutem zabraniającym wspomnianej transformacji. Również domniemane konwersje liczbowe mogłyby być znacznie lepsze, gdyby można je było oznaczyć, aby można je było stosować tylko w przypadkach, w których ich wywołanie byłoby oczywiste. Jeśli istnieją przeciążenia dla foo(float,float)i foo(double,double), przypuszczam, że próba użycia a floati doubleczęsto nie powinna stosować niejawnej konwersji, ale powinna być błędem.
supercat

Bezpośrednie przypisanie doublewartości do floatlub przekazanie jej do metody, która może przyjąć floatargument, ale nie double, prawie zawsze zrobiłoby to, co zamierzał programista. Natomiast przypisywanie floatwyrażenia doublebez jawnego typowania jest często błędem. Jedynym czasem, w którym możliwa jest niejawna double->floatkonwersja, byłyby problemy, gdy spowodowałoby to wybranie niecałkowitego przeciążenia. Sądzę, że właściwym sposobem, aby temu zapobiec, nie powinno być zakazywanie implcit double-> float, ale oznaczanie przeciążeń atrybutami, aby uniemożliwić konwersję.
supercat

8

Klasa jest typem referencyjnym. Kiedy tworzony jest obiekt klasy, zmienna, do której obiekt jest przypisany, zawiera tylko odwołanie do tej pamięci. Gdy odwołanie do obiektu jest przypisane do nowej zmiennej, nowa zmienna odnosi się do oryginalnego obiektu. Zmiany dokonane za pomocą jednej zmiennej są odzwierciedlone w drugiej zmiennej, ponieważ obie odnoszą się do tych samych danych. Struktura jest typem wartości. Po utworzeniu struktury zmienna, do której przypisana jest struktura, przechowuje rzeczywiste dane struktury. Kiedy struct jest przypisane do nowej zmiennej, jest kopiowane. Nowa zmienna i pierwotna zmienna zawierają zatem dwie osobne kopie tych samych danych. Zmiany wprowadzone w jednej kopii nie wpływają na drugą kopię. Zasadniczo klasy są używane do modelowania bardziej złożonych zachowań lub danych, które mają zostać zmodyfikowane po utworzeniu obiektu klasy.

Klasy i struktury (Podręcznik programowania w języku C #)


Struktury są również bardzo dobre w przypadkach, w których konieczne jest przymocowanie kilku powiązanych, ale niezależnych zmiennych wraz z taśmą klejącą (np. Współrzędne punktu). Wytyczne MSDN są rozsądne, jeśli próbuje się tworzyć struktury, które zachowują się jak obiekty, ale są znacznie mniej odpowiednie przy projektowaniu agregatów; niektóre z nich są prawie dokładnie błędne w tej drugiej sytuacji. Na przykład, im większy stopień niezależności zmiennych zawartych w typie, tym większa korzyść z zastosowania struktury pola wystawionego zamiast klasy niezmiennej.
supercat

6

MIT # 1: STRUKTY TO LEKKIE ZAJĘCIA

Ten mit ma różne formy. Niektóre osoby uważają, że typy wartości nie mogą lub nie powinny mieć metod lub innych istotnych zachowań - powinny być używane jako proste typy przesyłania danych, z tylko polami publicznymi lub prostymi właściwościami. Typ DateTime jest dobrym kontrprzykładem: ma sens, aby był typem wartości, ponieważ jest podstawową jednostką, taką jak liczba lub znak, a także ma sens, aby móc wykonywać obliczenia na podstawie jego wartość. Patrząc na to z drugiej strony, typy przesyłania danych i tak często powinny być typami referencyjnymi - decyzja powinna opierać się na pożądanej wartości lub semantyce typu referencyjnego, a nie na prostocie tego typu. Inne osoby uważają, że typy wartości są „lżejsze” niż typy referencyjne pod względem wydajności. Prawda jest taka, że ​​w niektórych przypadkach typy wartości są bardziej wydajne - nie wymagają wyrzucania elementów bezużytecznych, chyba że są zapakowane w pudełka, nie mają narzutu identyfikacji typu i nie wymagają na przykład dereferencji. Ale pod innymi względami typy referencyjne są bardziej wydajne - przekazywanie parametrów, przypisywanie wartości do zmiennych, zwracanie wartości i podobne operacje wymagają tylko 4 lub 8 bajtów do wywołania (w zależności od tego, czy korzystasz z 32-bitowego czy 64-bitowego CLR ) zamiast kopiowania wszystkich danych. Wyobraź sobie, że ArrayList jest w jakiś sposób „czystym” typem wartości, a przekazanie wyrażenia ArrayList do metody wymaga skopiowania wszystkich danych! W prawie wszystkich przypadkach wydajność nie jest tak naprawdę determinowana przez tego rodzaju decyzję. Wąskie gardła prawie nigdy nie są tam, gdzie sądzisz, a zanim podejmiesz decyzję dotyczącą projektu na podstawie wydajności, powinieneś zmierzyć różne opcje. Warto zauważyć, że połączenie dwóch przekonań również nie działa. Nie ma znaczenia, ile metod ma typ (bez względu na to, czy jest to klasa, czy struktura) - nie ma to wpływu na pamięć zajmowaną przez instancję. (Koszt jest związany z pamięcią zajmowaną przez sam kod, ale jest on ponoszony raz, a nie dla każdej instancji).

MIT 2: RODZAJE ODNIESIENIA ŻYJĄ W STOSUNKU; RODZAJE WARTOŚCI ŻYJĄ NA STOSOWIE

Często przyczyną jest lenistwo osoby powtarzającej się. Pierwsza część jest poprawna - instancja typu odwołania jest zawsze tworzona na stercie. To druga część, która powoduje problemy. Jak już zauważyłem, wartość zmiennej istnieje wszędzie tam, gdzie jest zadeklarowana, więc jeśli masz klasę ze zmienną instancji typu int, wartość tej zmiennej dla dowolnego obiektu zawsze będzie znajdować się tam, gdzie reszta danych dla tego obiektu - na stosie. Na stosie znajdują się tylko zmienne lokalne (zmienne zadeklarowane w ramach metod) i parametry metody. W C # 2 i późniejszych nawet niektóre zmienne lokalne tak naprawdę nie znajdują się na stosie, co zobaczysz, gdy przyjrzymy się anonimowym metodom w rozdziale 5. CZY TE POJĘCIA SĄ WŁAŚCIWE? Można argumentować, że jeśli piszesz kod zarządzany, powinieneś pozwolić środowisku wykonawczemu martwić się o to, jak najlepiej wykorzystać pamięć. W rzeczy samej, specyfikacja języka nie gwarantuje, co tam mieszka; przyszły środowisko wykonawcze może być w stanie utworzyć niektóre obiekty na stosie, jeśli wie, że może się z tym uwolnić, lub kompilator C # może wygenerować kod, który prawie nie używa stosu. Następny mit to zwykle tylko kwestia terminologii.

MIT 3: OBIEKTY SĄ PRZEDSTAWIONE PRZEZ ODNIESIENIE W C # BY DOMYŚLNIE

To prawdopodobnie najbardziej rozpowszechniony mit. Znów ludzie, którzy twierdzą to często (choć nie zawsze), wiedzą, jak naprawdę zachowuje się C #, ale nie wiedzą, co tak naprawdę oznacza „przekazanie przez odniesienie”. Niestety jest to mylące dla osób, które wiedzą, co to znaczy. Formalna definicja przekazywania przez referencję jest względnie skomplikowana i obejmuje wartości l i podobną terminologię informatyczną, ale ważne jest to, że jeśli przekazujesz zmienną przez referencję, metoda, którą wywołujesz, może zmienić wartość zmiennej wywołującej zmieniając wartość parametru. Pamiętajmy, że wartością zmiennej typu odwołania jest odwołanie, a nie sam obiekt. Można zmienić zawartość obiektu, do którego odnosi się parametr, bez przekazywania samego parametru przez odwołanie. Na przykład,

void AppendHello(StringBuilder builder)
{
    builder.Append("hello");
}

Po wywołaniu tej metody wartość parametru (odwołanie do StringBuilder) jest przekazywana przez wartość. Jeśli miałbyś zmienić wartość zmiennej konstruktora w ramach metody - na przykład za pomocą instrukcji builder = null; - zmiana ta nie byłaby widoczna dla wywołującego, w przeciwieństwie do mitu. Warto zauważyć, że nie tylko „odniesienie” do mitu jest niedokładne, ale także „przekazane przedmioty”. Same obiekty nigdy nie są przekazywane ani przez odwołanie, ani przez wartość. Gdy dotyczy typu odwołania, zmienna jest przekazywana przez odwołanie lub wartość argumentu (odwołanie) jest przekazywana przez wartość. Oprócz czegokolwiek innego, odpowiada to na pytanie, co się stanie, gdy wartość pusta zostanie użyta jako argument wartości - gdyby przekazano obiekty, spowodowałoby to problemy, ponieważ nie byłoby obiektu do przekazania! Zamiast, odwołanie zerowe jest przekazywane przez wartość w taki sam sposób, jak każde inne odwołanie. Jeśli to szybkie wyjaśnienie wprawiło Cię w osłupienie, możesz zajrzeć do mojego artykułu „Parametr przekazujący w C #” (http://mng.bz/otVt ), która zawiera więcej szczegółów. Te mity nie są jedynymi. Boks i rozpakowanie przychodzą z powodu ich uczciwego nieporozumienia, które postaram się wyjaśnić w następnej kolejności.

Odniesienie: C # w szczegółach 3. edycja autorstwa Jona Skeeta


1
Bardzo dobrze, zakładając, że masz rację. Również bardzo dobrze dodać referencję.
NoChance

5

Myślę, że dobrym pierwszym przybliżeniem jest „nigdy”.

Myślę, że dobrym drugim przybliżeniem jest „nigdy”.

Jeśli desperacko pragniesz perf, rozważ je, ale zawsze mierz.


24
Nie zgodziłbym się z tą odpowiedzią. Struktury mają uzasadnione zastosowanie w wielu scenariuszach. Oto przykład - zestawianie danych między procesami w sposób atomowy.
Franci Penov

25
Powinieneś edytować swój post i rozwinąć swoje punkty - wyraziłeś swoją opinię, ale powinieneś uzasadnić to, dlaczego ją przyjmujesz.
Erik Forbes,

4
Myślę, że potrzebują odpowiednika karty Totin 'Chip ( en.wikipedia.org/wiki/Totin%27_Chip ) do korzystania ze struktur. Poważnie.
Greg

4
W jaki sposób 87,5 tys. Osób publikuje taką odpowiedź? Czy zrobił to, gdy był dzieckiem?
Rohit Vipin Mathews

3
@Rohit - to było sześć lat temu; standardy witryny były wtedy bardzo różne. to wciąż zła odpowiedź, masz rację.
Andrew Arnold

5

Miałem właśnie do czynienia z Nazwaną potokiem Windows Communication Foundation [WCF] i zauważyłem, że warto używać Structów, aby upewnić się, że wymiana danych ma charakter wartości zamiast referencyjnego .


1
To najlepsza wskazówka ze wszystkich, IMHO.
Ivan

4

Struktura C # jest lekką alternatywą dla klasy. Może robić prawie to samo co klasa, ale tańsze jest używanie struktury zamiast klasy. Przyczyna tego jest nieco techniczna, ale podsumowując, nowe wystąpienia klasy są umieszczane na stercie, gdzie nowo tworzone struktury są umieszczane na stosie. Co więcej, nie masz do czynienia z odniesieniami do struktur, jak z klasami, ale zamiast tego pracujesz bezpośrednio z instancją struktury. Oznacza to również, że gdy przekazujesz strukturę do funkcji, ma ona wartość, a nie odwołanie. Więcej na ten temat w rozdziale dotyczącym parametrów funkcji.

Dlatego powinieneś używać struktur, gdy chcesz reprezentować prostsze struktury danych, a zwłaszcza, jeśli wiesz, że utworzysz wiele z nich. Istnieje wiele przykładów w .NET Framework, w których Microsoft używał struktur zamiast klas, na przykład struktury Point, Rectangle i Color.



3

Typy struktury lub wartości można zastosować w następujących scenariuszach -

  1. Jeśli chcesz zapobiec gromadzeniu obiektu przez odśmiecanie.
  2. Jeśli jest to typ prosty i żadna funkcja członka nie modyfikuje pól instancji
  3. Jeśli nie ma potrzeby czerpania z innych typów lub uzyskiwania innych typów.

Więcej informacji na temat typów wartości i typów wartości można znaleźć tutaj, pod tym linkiem


3

W skrócie, użyj struct, jeśli:

1- właściwości / pola obiektu nie muszą być zmieniane. Mam na myśli, że chcesz tylko nadać im wartość początkową, a następnie je przeczytać.

2- właściwości i pola w twoim obiekcie są typem wartości i nie są tak duże.

W takim przypadku możesz skorzystać z struktur dla lepszej wydajności i zoptymalizowanego przydziału pamięci, ponieważ używają one tylko stosów, a nie zarówno stosów, jak i stosów (w klasach)


2

Rzadko używam struktury do różnych rzeczy. Ale to tylko ja. Zależy to od tego, czy potrzebuję, aby obiekt był zerowalny, czy nie.

Jak stwierdzono w innych odpowiedziach, używam klas dla obiektów w świecie rzeczywistym. Mam również sposób myślenia o strukturach używanych do przechowywania niewielkich ilości danych.


-11

Struktury są pod wieloma względami podobne do klas / obiektów. Struktura może zawierać funkcje, elementy i może być dziedziczona. Ale struktury są w C # używane tylko do przechowywania danych . Struktury zajmują mniej pamięci RAM niż klasy i są łatwiejsze do zebrania przez śmieciarz . Ale kiedy używasz funkcji w swojej strukturze, kompilator faktycznie przyjmuje tę strukturę bardzo podobnie do klasy / obiektu, więc jeśli chcesz czegoś z funkcjami, użyj klasy / obiektu .


2
Struktury NIE mogą być dziedziczone, patrz msdn.microsoft.com/en-us/library/0taef578.aspx
HimBromBeere 10.1015
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.