2014-12-15 14 views
32

Biorąc pod uwagę następujący kod:Zrozumienie C Wymagania inicjalizacji # pole

public class Progressor 
{ 
    private IProgress<int> progress = new Progress<int>(OnProgress); 

    private void OnProgress(int value) 
    { 
     //whatever 
    } 
} 

To daje następujący błąd na kompilacji:

A field initializer cannot reference the non-static field, method, or property 'Progressor.OnProgress(int)'

Rozumiem, ograniczenie to nie narzeka, ale ja nie rozumiem dlaczego jest to problem, ale pole można zainicjować w konstruktorze w następujący sposób:

public class Progressor 
{ 
    private IProgress<int> progress; 

    public Progressor() 
    { 
     progress = new Progress<int>(OnProgress); 
    } 

    private void OnProgress(int value) 
    { 
     //whatever 
    } 
} 

Jaka jest różnica w C# w odniesieniu do inicjalizacji pola w porównaniu do inicjacji konstruktora, która wymaga tego ograniczenia?

+7

Jedyne, co przychodzi mi na myśl, to fakt, że gdy uruchomiona jest konkretna inicjalizacja pola, wszystkie inne pola mogą być zainicjalizowane lub nie. Uruchomienie w tym miejscu metody instancji, która dawałaby dostęp do wszystkich członków instancji, umożliwi dostęp do innych pól, które nie zostały jeszcze zainicjowane. To potencjalne źródło problemów. Dlaczego jest dozwolony w konstruktorze? Ponieważ wszystkie pola są inicjowane przed uruchomieniem kodu konstruktora. – MarcinJuraszek

+0

@MarcinJuraszek To było moje zrozumienie, chociaż można by argumentować, że środowisko wykonawcze może po prostu zainicjować wszystkie pola do ich wartości domyślnych (tj. 0, null, itd.), A następnie uruchomić inicjalizacje pól, a następnie uruchomić odpowiedni konstruktor. – sdzivanovich

+2

@sdzivanovich Dokładnie to się dzieje - wszystkie pola zostają zainicjowane wartościami domyślnymi, zanim nastąpi inicjalizacja.Ale nadal nie rozwiązuje problemu nieprzewidywalności kodu inicjalizacyjnego (zobacz moją odpowiedź). – MarcinJuraszek

Odpowiedz

22

Inicjalizacja pól przychodzi przed wywołaniem konstruktora klasy podstawowej, więc nie jest poprawnym obiektem. Każde wywołanie metody z this jako argumentem w tym miejscu prowadzi do niezweryfikowanego kodu i wyrzucenia VerificationException, jeśli nie można zweryfikować kodu. Na przykład: w przezroczystym kodzie zabezpieczeń.

  • 10.11.2 Instance variable initializers
    When an instance constructor has no constructor initializer, or it has a constructor initializer of the form base(...), that constructor implicitly performs the initializations specified by the variable-initializers of the instance fields declared in its class. This corresponds to a sequence of assignments that are executed immediately upon entry to the constructor and before the implicit invocation of the direct base class constructor. The variable initializers are executed in the textual order in which they appear in the class declaration.
  • 10.11.3 Constructor execution
    Variable initializers are transformed into assignment statements, and these assignment statements are executed before the invocation of the base class instance constructor. This ordering ensures that all instance fields are initialized by their variable initializers before any statements that have access to that instance are executed.
+0

Zweryfikowany przez programistę kompilatora C# [tutaj] (http://blogs.msdn.com/b/ericlippert/archive/2008/02/15/why-do-initializers-run-in-the-opposite-order-as -constructors-part-one.aspx) i [tutaj] (http://blogs.msdn.com/b/ericlippert/archive/2008/02/18/why-do-initializers-run-in-the-opposite- order-as-constructors-part-two.aspx) – Yishai

+3

Wymagane jest zgłoszenie, które doprowadziłoby do 'VerificationException'. Faktycznie testowanie go w nowej aplikacji konsoli (generowanie poprawnego IL ręcznie zamiast wypróbowania go z C#) pokazuje, że działa dobrze. Z pewnością jest możliwe, że zawiedzie w niektórych środowiskach, ale nie w innych, ale wtedy twoja odpowiedź powinna nadal wskazywać, że niekoniecznie zawiedzie. – hvd

+1

@hvd Nie jestem pewien, czy mogę podać odniesienie do specyfikacji (ja sam lubię znaleźć specyfikację procesu weryfikacji .NET). W mojej metodzie testowej wywołanie z 'this' jako argumentem przed wywołaniem konstruktora klasy podstawowej zawsze wyrzuć' VerificationException' w przezroczystym kodzie bezpieczeństwa. W przypadku kodu krytycznego pod względem bezpieczeństwa nie zgłoszono wyjątku, ale komunikat błędu PEVerify: " ref (" ten "ptr)" ClassName "" zamiast "ref" ClassName "". stan tego "powoduje, że każde wywołanie nie jest możliwe do zweryfikowania. – PetSerAl

9

Sekcja 10.5.5.2: Instance inicjalizacji pola opisuje ten problem:

A variable initializer for an instance field cannot reference the instance being created. Thus, it is a compile-time error to reference this in a variable initializer, as it is a compile-time error for a variable initializer to reference any instance member through a simple-name

Takie zachowanie ma zastosowanie do kodu ponieważ OnProgress jest niejawna odwołanie do instancji tworzone.

+5

Dobrze. Sądzę, że OP wie, dlaczego dostał i co za błąd. Pytanie dotyczy raczej tego, dlaczego w ogóle jest to ograniczenie. – MarcinJuraszek

+4

@MarcinJuraszek: To prawda, ale trudno będzie odpowiedzieć bez projektanta języka C# –

7

Odpowiedź brzmi mniej więcej, projektanci C# preferowali to w ten sposób.

Ponieważ wszystkie inicjatory polowe są tłumaczone na instrukcje w konstruktorze, które występują przed innymi instrukcjami w konstruktorze, nie ma technicznych przyczyn, dla których nie byłoby to możliwe. Jest to więc wybór projektu.

Dobrą cechą konstruktora jest to, że wyjaśnia, w jakiej kolejności wykonywane są zadania.

Należy zauważyć, że z członkami static, projektanci C# postanowili inaczej. Np

static int a = 10; 
static int b = a; 

jest dozwolone, i różni się od tego (również możliwość)

static int b = a; 
static int a = 10; 

co może być mylące.

Jeśli się:

partial class C 
{ 
    static int b = a; 
} 

i gdzie indziej (w innym pliku):

partial class C 
{ 
    static int a = 10; 
} 

ja nawet nie myślę, że jest dobrze zdefiniowany, co się wydarzy.

oczywiście dla konkretnego przykładu z delegatów do inicjowania instancji pola:

Action<int> progress = OnProgress; // ILLEGAL (non-static method OnProgress) 

naprawdę nie ma problemu, ponieważ to nie jest tylko do odczytu lub wywołanie członu non-statycznego. Zamiast tego używa się informacji o metodach i nie zależy to od jakiejkolwiek inicjalizacji. Ale zgodnie ze specyfikacją języka C# nadal jest to błąd podczas kompilacji.

+0

Twój ostatni przykład z delegatem jest w rzeczywistości błędny. To przypisanie nie wykorzystuje po prostu informacji o metodzie, ale musi skonstruować instancję delegata, która obejmuje "to", i dlatego generuje ten sam wyjątek. – Bunny83

+1

@ Bunny83 Tak, wiem. Próbowałem teraz edytować mój post, aby było bardziej zrozumiałe. W tym ostatnim przykładzie kompilator musiałby zrobić coś _równomiernego_ do 'progress = Delegate.CreateDelegate (typeof (Działanie ), to," OnProgress ")', więc z pewnością wymaga przekazania odniesienia do 'this' (które będzie przechowywane przez delegata), tak jak mówisz. A to nie jest dozwolone. Nawet jeśli odwołanie nie zostanie "zastosowane" do późniejszego czasu. –

18

Wszystko w mojej odpowiedzi to tylko moje przemyślenia na temat "dlaczego niebezpiecznie byłoby zezwolić na ten rodzaj dostępu". Nie wiem, czy to był prawdziwy powód, dla którego był ograniczony.

C# specyfikacja mówi, że inicjalizacji pola dzieje się w dziedzinie zamówień są zadeklarowane w klasie:

10.5.5.2. Instance field initialization

The variable initializers are executed in the textual order in which they appear in the class declaration.

Teraz, powiedzmy, że kod już wspomniano jest to możliwe - można wywołać metodę instancji z pola inicjalizacja. Może to zrobić następujący kod:

public class Progressor 
{ 
    private string _first = "something"; 
    private string _second = GetMyString(); 

    private string GetMyString() 
    { 
     return "this is really important string"; 
    } 
} 

Jak dotąd tak dobrze. Ale bądźmy nadużywają tej władzy trochę:

public class Progressor 
{ 
    private string _first = "something"; 
    private string _second = GetMyString(); 
    private string _third = "hey!"; 

    private string GetMyString() 
    { 
     _third = "not hey!"; 
     return "this is really important string"; 
    } 
} 

Więc _second get zainicjowany przed _third. GetMyString działa, _third get "nie hej!" przypisana wartość, ale później uruchamia się własna inicjalizacja pola i jest ustawiana na "" hej! ". Niezbyt przydatne, czytelne, prawda?

Można również użyć _third ciągu GetMyString metody:

public class Progressor 
{ 
    private string _first = "something"; 
    private string _second = GetMyString(); 
    private string _third = "hey!"; 

    private string GetMyString() 
    { 
     return _third.Substring(0, 1); 
    } 
} 

Co można oczekiwać, aby być wartość _second? Cóż, przed uruchomieniem inicjalizacji pola wszystkie pola otrzymują wartości domyślne. Dla string będzie to null, więc otrzymasz niespodziewany NullReferenceException.

Tak więc projektanci zdecydowali, że łatwiej jest zapobiec takim błędom.

Można powiedzieć: OK, niech zabraknie dostępu do właściwości i metod wywoływania, ale pozwólmy na użycie pól, które zostały zadeklarowane powyżej tego, z którego chcesz uzyskać do niego dostęp. Coś jak:

public class Progressor 
{ 
    private string _first = "something"; 
    private string _second = _first.ToUpperInvariant(); 
} 

ale nie

public class Progressor 
{ 
    private string _first = "something"; 
    private string _second = _third.ToUpperInvariant(); 
    private string _third = "another"; 
} 

To wydaje się użyteczne i bezpieczne. Ale wciąż jest sposób na jej nadużycie!

public class Progressor 
{ 
    private Lazy<string> _first = new Lazy<string>(GetMyString); 
    private string _second = _first.Value; 

    private string GetMyString() 
    { 
     // pick one from above examples 
    } 
} 

I wszystkie problemy z metodami powracają.

+0

Ale jeśli nalegasz, wciąż możesz napisać "_pierwszy =" coś "; _second = this.GetMyString(); _third = "hej!"; 'wewnątrz konstruktora instancji. Oczywiście można uzyskać 'NullReferenceException', jeśli' GetMyString' użył pola pustego. –

+0

Ale pisanie tego w konstruktorze czyni bardziej oczywistym, co się stanie. – MarcinJuraszek

+1

Zgadzam się. Spekuluję również, że to jest powód, dla którego nie pozwolą na to z inicjalizatorami pól. Ciekawe jest to, że wszystkie "niebezpieczne" przykłady podane powyżej, stają się całkowicie legalne, jeśli sprawisz, że wszyscy członkowie klasy będą "statyczni". To właśnie próbowałem opisać w mojej odpowiedzi. –