parametry typu struct nie muszą być przypisane

9

Zauważyłem dziwne zachowanie w kodzie podczas przypadkowego komentowania linii w funkcji podczas przeglądania kodu. Bardzo trudno było to odtworzyć, ale przedstawię tutaj podobny przykład.

Mam tę klasę testową:

public class Test
{
    public void GetOut(out EmailAddress email)
    {
        try
        {
            Foo(email);
        }
        catch
        {
        }
    }

    public void Foo(EmailAddress email)
    {
    }
}

nie ma przypisania do wiadomości e-mail, w GetOutktórym zwykle zgłaszany jest błąd:

Do parametru wyjściowego „e-mail” należy przypisać, zanim kontrola opuści bieżącą metodę

Jednak jeśli EmailAddress znajduje się w strukturze w oddzielnym zestawie, nie powstaje błąd i wszystko się kompiluje.

public struct EmailAddress
{
    #region Constructors

    public EmailAddress(string email)
        : this(email, string.Empty)
    {
    }

    public EmailAddress(string email, string name)
    {
        this.Email = email;
        this.Name = name;
    }

    #endregion

    #region Properties

    public string Email { get; private set; }
    public string Name { get; private set; }

    #endregion
}

Dlaczego kompilator nie wymusza przypisania tego adresu e-mail? Dlaczego ten kod kompiluje się, jeśli struktura jest tworzona w oddzielnym zestawie, ale nie kompiluje się, jeśli struktura jest zdefiniowana w istniejącym zestawie?

Johnny 5
źródło
2
Jeśli używasz klasy, musisz „nowe” wystąpienie obiektu. Nie jest wymagane dla struktur. docs.microsoft.com/en-us/dotnet/csharp/programming-guide/... (wyszukaj ten tekst na tej stronie, w szczególności: W przeciwieństwie do klas, struktury mogą być tworzone bez użycia nowego
opera
1
Gdy tylko Twój pies otrzyma zmienną, nie skompiluje się :)
André Sanson
W tym przykładzie z struct Dog{}, wszystko jest w porządku.
Henk Holterman
2
@ johnny5 Następnie pokaż przykład.
André Sanson
1
OK, to interesujące. Reprodukcja z aplikacją Core 3 Console i biblioteką klasy .Standard.
Henk Holterman

Odpowiedzi:

12

TLDR: Jest to znany błąd od dawna. Pierwszy raz o tym napisałem w 2010 roku:

https://blogs.msdn.microsoft.com/ericlippert/2010/01/18/a-definite-assignment-anomaly/

Jest nieszkodliwy i możesz go bezpiecznie zignorować i pogratulować sobie znalezienia nieco niejasnego błędu.

Dlaczego kompilator nie wymusza tego, że Emailnależy go definitywnie przypisać?

Och, robi to w pewien sposób. Po prostu źle rozumie, jaki warunek implikuje, że zmienna jest definitywnie przypisana, jak zobaczymy.

Dlaczego ten kod kompiluje się, jeśli struktura jest tworzona w oddzielnym zestawie, ale nie kompiluje się, jeśli struktura jest zdefiniowana w istniejącym zestawie?

To sedno błędu. Błąd jest konsekwencją przecięcia się tego, w jaki sposób kompilator C # wykonuje określone sprawdzanie przypisania struktur i jak kompilator ładuje metadane z bibliotek.

Rozważ to:

struct Foo 
{ 
  public int x; 
  public int y; 
}
// Yes, public fields are bad, but this is just 
// to illustrate the situation.
void M(out Foo f)
{

OK, w tym momencie co wiemy? fjest aliasem zmiennej typu Foo, więc pamięć została już przydzielona i na pewno jest przynajmniej w stanie, w którym wyszła z alokatora pamięci. Jeśli obiekt wywołujący umieścił wartość w zmiennej, ta wartość jest dostępna.

Czego potrzebujemy? Wymagamy, aby fzostało to definitywnie przypisane w każdym punkcie, w którym kontrola Mnormalnie wychodzi . Więc można się spodziewać czegoś takiego:

void M(out Foo f)
{
  f = new Foo();
}

które ustawia f.xi f.yich wartości domyślne. Ale co z tym?

void M(out Foo f)
{
  f = new Foo();
  f.x = 123;
  f.y = 456;
}

To też powinno być w porządku. Ale tutaj jest kicker, dlaczego musimy przypisać wartości domyślne tylko po to, aby je zdmuchnąć chwilę później? Określony kontroler przypisania C # sprawdza, czy każde pole jest przypisane! To jest legalne:

void M(out Foo f)
{
  f.x = 123;
  f.y = 456;
}

I dlaczego nie powinno to być legalne? To typ wartości. fjest zmienną i zawiera już prawidłową wartość typu Foo, więc po prostu ustawmy pola i gotowe, prawda?

Dobrze. Więc jaki jest błąd?

Wykryty przez Ciebie błąd: kompilator C # nie jest ładowany w celu oszczędności kosztów metadanych dla prywatnych pól struktur znajdujących się w bibliotekach, do których się odwołuje . Te metadane mogą być ogromne i spowalniałyby kompilator przy bardzo małej wygranej, aby za każdym razem ładować je do pamięci.

A teraz powinieneś być w stanie wywnioskować przyczynę znalezionego błędu. Gdy kompilator sprawdza, czy parametr out jest definitywnie przypisany, porównuje liczbę znanych pól z liczbą pól, które zostały zdecydowanie zainicjowane, aw twoim przypadku wie tylko o zerowych polach publicznych, ponieważ metadane pola prywatnego nie zostały załadowane . Kompilator stwierdza: „wymagane zero pól, zainicjowanie zerowych pól, jesteśmy dobrzy”.

Jak powiedziałem, ten błąd istnieje od ponad dekady, a ludzie tacy jak ty czasami go odkrywają i zgłaszają. Jest nieszkodliwy i jest mało prawdopodobne, aby został naprawiony, ponieważ jego naprawa przynosi prawie zerową korzyść, ale wysoki koszt wydajności.

I oczywiście błąd nie wypowiada się na prywatne pola struktur, które są w kodzie źródłowym w twoim projekcie, ponieważ oczywiście kompilator ma już informacje o dostępnych polach prywatnych.

Eric Lippert
źródło
@ johnny5: Nie powinieneś dostawać błędów. Zobacz dotnetfiddle.net/ZEKiUk . Czy możesz opublikować proste repro?
Eric Lippert,
1
Dzięki za skrzypce było tak, ponieważ zdefiniowałem xiy jako właściwości zamiast członków
Johnny 5
1
@ johnny5: Jeśli właśnie zdefiniowałeś normalną właściwość stylu C # 1.0, to z perspektywy kontrolera przypisania jest to metoda, a nie pole. Jeśli zdefiniowano właściwość automatyczną w stylu C # 3.0+, kompilator wie, że istnieje prywatne pole, które ją popiera; zasady definitywnego przypisania tej rzeczy były modyfikowane przez lata i nie pamiętam teraz dokładnych zasad.
Eric Lippert,
Jeśli użyjesz System.TimeSpanzamiast tego, pojawiają się błędy: error CS0269: Use of unassigned out parameter 'email'i error CS0177: The out parameter 'email' must be assigned to before control leaves the current method. Jest tylko jedno pole niestatyczne TimeSpan, mianowicie _ticks. To jest internaldo jego złożenia mscorlib. Czy ten zespół jest wyjątkowy? To samo dotyczy System.DateTime, a jego pole toprivate
Jeppe Stig Nielsen
@JeppeStigNielsen: Nie wiem o co chodzi! Jeśli to wymyślisz, daj mi znać.
Eric Lippert
1

Choć wygląda jak błąd, ma jakiś sens.

„Brakujący błąd” pojawia się tylko podczas korzystania z biblioteki klas. A biblioteka klas mogła zostać napisana w innym języku .net, np. VB.Net. „Śledzenie określonego przypisania” jest funkcją C #, a nie frameworka.

Ogólnie rzecz biorąc, nie sądzę, że to błąd, ale nie wiem o autorytatywnym stwierdzeniu w tym zakresie.

Henk Holterman
źródło
Jeśli importujesz zestaw do C #, mimo że struktura może leżeć w zestawie napisanym w innym języku, kod, który go używa, wciąż znajduje się w c #, więc nie powinien używać określonego śledzenia przypisania.
Johnny 5
1
Niekoniecznie. C # nie pozwala na użycie zmiennej unitializowanej (lokalnej), ale jednocześnie framework gwarantuje, że zostanie ustawiona na 0 ( default(T)). Więc nie ma naruszenia bezpieczeństwa pamięci ani czegoś podobnego.
Henk Holterman
3
Mogę złożyć takie autorytatywne oświadczenie. :) To znany od dawna błąd.
Eric Lippert,
1
@EricLippert Dzięki, miałem nadzieję, że zobaczysz to w pewnym momencie
Johnny 5