Dlaczego delegaci są typami referencyjnymi?

Krótka uwaga na zaakceptowaną odpowiedź : nie zgadzam się z małą częścią odpowiedzi Jeffreya , a mianowicie z punktem, że ponieważ Delegate musi być typem referencyjnym, wynika z tego, że wszyscy delegaci są typami referencyjnymi. (Po prostu nie jest prawdą, że wielopoziomowy łańcuch dziedziczenia wyklucza typy wartości; wszystkie typy enum, na przykład, dziedziczą z System.Enum, które z kolei dziedziczą z System.ValueType, które dziedziczą z System.Object, wszystkie typy referencyjne.) Jednak myślę, że fakt, że, zasadniczo, wszyscy delegaci w rzeczywistości dziedziczą nie tylko z Delegate, ale z MulticastDelegate jest krytyczna realizacja tutaj. Jak Raymond zwraca uwagę w komentarzu do jego odpowiedzi, kiedy już zobowiązałeś się do wspierania wielu subskrybentów, naprawdę nie ma sensu nie używać typu referencyjnego dla samego delegata, biorąc pod uwagę potrzebę posiadania gdzieś tablicy.


Patrz aktualizacja na dole.

Zawsze wydawało mi się dziwne, że jeśli to:

Action foo = obj.Foo;

Tworzę nowy Action sprzeciw, za każdym razem. Jestem pewien, że koszt jest minimalny, ale wiąże się z alokacją pamięci, aby później być zbierane śmieci.

Biorąc pod uwagę, że delegaty są z natury same w sobie niezmienne, zastanawiam się, dlaczego nie mogą być typami wartości? Wtedy linia kodu, taka jak ta powyżej, nie pociągnie za sobą nic więcej niż proste przypisanie do adresu pamięci na stosie*.

Nawet biorąc pod uwagę funkcje anonimowe, wydaje się, że (do ja ) to by zadziałało. Rozważ następujący prosty przykład.

Action foo = () => { obj.Foo(); };

W Tym Przypadku foo stanowi zamknięcie , tak. I w wielu przypadkach, wyobrażam sobie, że wymaga to rzeczywistego typu odniesienia(na przykład, gdy zmienne lokalne są zamknięte i są modyfikowane w ramach zamknięcia). ale w niektórych przypadkach nie powinno. na przykład w powyższym przypadku, wydaje się, że typ wspierający zamknięcie może wyglądać tak: cofam swój pierwotny punkt o to. Poniżej naprawdę musi być Typ odniesienia (lub: it doesn ' t need to be, ale jeśli to jest {[12] }to po prostu zostanie boxed anyway). Zignoruj poniższy przykład kodu. Zostawiam to tylko po to, aby dostarczyć kontekstu dla odpowiedzi, o których konkretnie mowa.

struct CompilerGenerated
{
    Obj obj;

    public CompilerGenerated(Obj obj)
    {
        this.obj = obj;
    }

    public void CallFoo()
    {
        obj.Foo();
    }
}

// ...elsewhere...

// This would not require any long-term memory allocation
// if Action were a value type, since CompilerGenerated
// is also a value type.
Action foo = new CompilerGenerated(obj).CallFoo;
Czy to pytanie ma sens? Jak to widzę, istnieją dwa możliwe wyjaśnienia:
  • poprawne Implementowanie delegatów jako typów wartości wymagałoby dodatkowej pracy / złożoności, ponieważ wsparcie dla takich rzeczy jak zamknięcia, które modyfikują wartości zmiennych lokalnych, i tak wymagałoby generowanych przez kompilator typów referencyjnych.
  • istnieją pewne inne powody, dla których delegaty po prostu nie mogą być zaimplementowane jako typy wartości.

W końcu, nie tracę przez to snu; to jest po prostu coś, co byłem ciekawy od jakiegoś czasu.


Update: w odpowiedzi na komentarz Ani widzę dlaczego typ CompilerGenerated w moim powyższym przykładzie równie dobrze może być typem referencyjnym, ponieważ jeśli delegat ma zawierać wskaźnik funkcji i wskaźnik obiektu, to i tak będzie potrzebował typu referencyjnego (przynajmniej dla funkcji anonimowych używających zamknięć, ponieważ nawet jeśli wprowadzono dodatkowy parametr typu ogólnego-np. Action<TCaller>-to nie obejmowałoby typów, których nie można nazwać!). jednak , wszystko to sprawia, że żałuję, że pytanie o typy generowane przez kompilator dla zamknięć w dyskusja w ogóle! Moje główne pytanie dotyczy delegatów, czyli rzeczy z wskaźnikiem funkcji i wskaźnikiem obiektu. Wciąż wydaje mi się, że może być typem wartości.

Innymi słowy, nawet jeśli to...
Action foo = () => { obj.Foo(); };

...wymaga utworzenia jednego obiektu typu referencyjnego (aby wspierać zamknięcie i dać delegatowi coś do odwołania), dlaczego wymaga utworzenia dwóch (obiekt wspierający zamknięcie plus Action delegat)?

*tak, tak, szczegóły realizacji, wiem! Chodzi mi tylko o pamięć krótkotrwałą.

Author: Community, 2011-10-26

7 answers

Pytanie sprowadza się do tego: Specyfikacja CLI (Common Language Infrastructure) mówi, że delegaci są typami referencyjnymi. Dlaczego tak jest?

Jeden z powodów jest dziś wyraźnie widoczny w.NET Framework. W pierwotnym projekcie istniały dwa rodzaje delegatów: delegaci normalni i delegaci "multicast", którzy mogli mieć więcej niż jeden cel na liście zaproszeń. Klasa MulticastDelegate dziedziczy z Delegate. Ponieważ nie można dziedziczyć po typie wartości, Delegate musiało być referencją Typ.

W końcu, wszyscy faktyczni delegaci skończyli jako delegaci multicastu, ale na tym etapie procesu było za późno, aby połączyć obie klasy. Zobacz ten post na blogu {[10] } na ten temat:

Porzuciliśmy rozróżnienie między delegatem a Multicastdelegatem pod koniec V1. W tym czasie byłby to ogromny Zmień, aby połączyć dwie klasy, więc nie zrobiliśmy tego. Powinieneś udawać, że są połączone i że tylko Istnieje MulticastDelegate.

Ponadto delegaci mają obecnie 4-6 pól, wszystkie wskaźniki. 16 bajtów jest zwykle uważane za górną granicę, gdzie zapisanie pamięci nadal wygrywa z dodatkowym kopiowaniem. 64-bitowy MulticastDelegate zajmuje 48 bajtów. Biorąc to pod uwagę i fakt, że używali dziedziczenia, sugeruje, że klasa była naturalnym wyborem.

 16
Author: Jeffrey Sax,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-10-26 18:29:18

Jest tylko jeden powód, dla którego Delegat musi być klasą, ale jest to duży: chociaż delegat może być wystarczająco mały, aby umożliwić wydajne przechowywanie jako typ wartości( 8 bajtów w systemach 32-bitowych lub 16 bajtów w systemach 64-bitowych), nie ma możliwości, aby był wystarczająco mały, aby skutecznie zagwarantować, że jeden wątek spróbuje napisać delegata, podczas gdy inny wątek spróbuje go wykonać, ten ostatni wątek nie skończy się ani wywołaniem starej metody na nowym obiekcie docelowym, ani nowej metody na Starym cel. Dopuszczenie do takiej sytuacji byłoby poważną dziurą bezpieczeństwa. Posiadanie delegatów jako typów referencyjnych pozwala uniknąć tego ryzyka.

Właściwie, nawet lepiej niż mieć typy struktur delegatów, byłoby mieć je jako interfejsy. Tworzenie zamknięcia wymaga utworzenia dwóch obiektów sterty: obiektu generowanego przez kompilator, który przechowuje wszystkie zmienne zamknięte, oraz delegata, który wywołuje właściwą metodę na tym obiekcie. Jeśli delegaty były interfejsami, obiekt, który posiadał zmienne zamknięte sam może być używany jako delegat, bez potrzeby stosowania innych obiektów.

 9
Author: supercat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-11-29 22:47:49

Wyobraź sobie, że delegaty były typami wartości.

public delegate void Notify();

void SignalTwice(Notify notify) { notify(); notify(); }

int counter = 0;
Notify handler = () => { counter++; }
SignalTwice(handler);
System.Console.WriteLine(counter); // what should this print?

Zgodnie z Twoją propozycją, to zostanie wewnętrznie przekonwertowane na

struct CompilerGenerated
{
    int counter = 0;
    public Execute() { ++counter; }
};

Notify handler = new CompilerGenerated();
SignalTwice(handler);
System.Console.WriteLine(counter); // what should this print?

Jeśli delegate są typem wartości, to SignalEvent otrzyma kopię handler, co oznacza, że zostanie utworzona nowa CompilerGenerated (Kopia handler) i przekazana do SignalEvent. SignalTwice wykonywałby delegata dwukrotnie, co zwiększa counter dwukrotnie W kopii. A następnie SignalTwice zwraca, a funkcja wypisuje 0, ponieważ oryginał nie został zmodyfikowany.

 7
Author: Raymond Chen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-10-26 17:57:05

Oto niedoinformowane zgadywanie:

Gdyby delegaty były zaimplementowane jako typy wartości, instancje byłyby bardzo kosztowne w kopiowaniu, ponieważ instancje delegatów są stosunkowo ciężkie. Być może MS uważało, że bezpieczniej byłoby zaprojektować je jako niezmienne typy referencji - kopiowanie maszynowe-odniesienia do instancji są stosunkowo tanie.

Instancja delegata potrzebuje przynajmniej:

  • odniesienie do obiektu ("to" odniesienie do metody zawiniętej, jeśli jest metodą instancji).
  • Wskaźnik do funkcji zawiniętej.
  • odniesienie do obiektu zawierającego listę wywołań multicastu. Należy zauważyć, że typ delegata powinien wspierać, z założenia, multicast przy użyciu tego samego typu delegata .

Załóżmy, że delegaty typu wartości zostały zaimplementowane w sposób podobny do obecnej implementacji typu odniesienia (jest to być może nieco nieuzasadnione; inny projekt mógł zostać wybrany, aby utrzymać rozmiar dół) do zilustrowania. Używając Reflector, oto pola wymagane w instancji delegata:

System.Delegate: _methodBase, _methodPtr, _methodPtrAux, _target
System.MulticastDelegate: _invocationCount, _invocationList

Jeśli zostanie zaimplementowana jako struktura (bez nagłówka obiektu), dodadzą się do 24 bajtów na x86 i 48 bajtów na x64, co jest ogromne jak na strukturę.


Z innej uwagi, chcę zapytać, jak w Twoim projekcie, tworzenie struktury CompilerGenerated typu zamkniętego a pomaga w jakikolwiek sposób. Gdzie miałby wskazywać wskaźnik obiektu utworzonego delegata? Pozostawienie instancji typu closure na stosie bez właściwa analiza ucieczki byłaby ekstremalnie ryzykownym biznesem.

 4
Author: Ani,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-10-26 17:57:04

Mogę powiedzieć, że tworzenie delegatów jako typów referencyjnych jest zdecydowanie złym wyborem projektowym. Mogą one być typami wartości i nadal wspierać delegatów z wieloma rzutami.

Wyobraź sobie, że delegat jest strukturą złożoną z, powiedzmy: obiekt docelowy; wskaźnik do metody

To może być struktura, prawda? Boks pojawi się tylko wtedy, gdy celem jest struktura (ale sam delegat nie będzie boksowany).

Możesz myśleć, że nie będzie obsługiwać MultiCastDelegate, ale wtedy my can: Utwórz nowy obiekt, który będzie zawierał tablicę zwykłych delegatów. Zwraca delegata (jako strukturę) do tego nowego obiektu, który zaimplementuje Invoke iterując wszystkie jego wartości i wywołując na nich Invoke.

Więc dla zwykłych delegatów, którzy nigdy nie będą wywoływać dwóch lub więcej funkcji obsługi, może to działać jako struktura. Niestety, to się nie zmieni w .Net.


Na marginesie, wariancja nie wymaga, aby Delegat był typem referencyjnym. Parametry delegat powinien być typem referencyjnym. W końcu, jeśli przekazujesz ciąg znaków, w którym jest wymagany obiekt (do wejścia, nie ref lub out), to nie jest potrzebny cast, ponieważ string jest już obiektem.

 2
Author: Paulo Zemek,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-03-19 19:08:14

Widziałem taką ciekawą rozmowę w Internecie:

Immutable nie oznacza, że musi być typem wartości. I coś, co is a value type is not required to be immutable. Te dwa często idą ręka w rękę, ale to nie to samo, a są w rzeczywistości przeciw-przykłady każdego w. NET Framework (ciąg klasy, na przykład).

I odpowiedź:

Różnica polega na tym, że podczas gdy niezmienne typy odniesienia są dość powszechne i całkowicie rozsądne, dzięki czemu typy wartości można zmieniać jest prawie zawsze złym pomysłem i może spowodować pewne bardzo mylące zachowanie!

Wzięte z tutaj

Tak więc, moim zdaniem decyzja była podejmowana przez aspekty użyteczności języka, a nie przez trudności technologiczne kompilatora. Kocham nullable delegatów.

 2
Author: Daniel Peñalba,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-05-11 11:28:41

Myślę, że jednym z powodów jest wsparcie dla delegatów multi cast Delegaty multi Cast są bardziej złożone niż kilka pól wskazujących cel i metodę.

Inną rzeczą, która jest możliwa tylko w tej formie, jest wariancja delegatów. Ten rodzaj wariancji wymaga konwersji odniesienia między dwoma typami.

Co ciekawe F # definiuje własny typ wskaźnika funkcji, który jest podobny do delegatów, ale jest bardziej lekki. Ale nie jestem pewien, czy to wartość czy typ odniesienia.

 1
Author: CodesInChaos,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-10-26 17:07:18