24

Otworzyłem nasze rozwiązanie w Visual Studio 2015 wczoraj i kilka naszych testów jednostkowych (które działały dobrze w Visual Studio 2013) zaczynało się nie udać. Odkrywam głębiej Odkryłem, że to dlatego, że wywołanie GetTypes() na zespole zwróciło inne wyniki. Byłem w stanie stworzyć bardzo prosty przypadek testowy, aby go zilustrować.Zachowanie Assembly.GetTypes() zmienione w Visual Studio 2015

W obu Visual Studio 2013 i 2015 utworzyłem nową aplikację konsolową z .NET Framework 4.5.2. W obu projektach umieszczam poniższy kod.

class Program 
{ 
    static void Main(string[] args) 
    { 
     var types = typeof(Program).Assembly.GetTypes() 
       .Where(t => !t.IsAbstract && t.IsClass); 

     foreach (var type in types) 
     { 
      Console.WriteLine(type.FullName); 
     } 

     Console.ReadKey(); 
    } 
} 

Po uruchomieniu programu Visual Studio 2013 otrzymuję następujące dane wyjściowe (zgodnie z oczekiwaniami).

VS2013Example.Program

Kiedy biegnę w Visual Studio 2015 pojawia się następujący komunikat (nie jak oczekiwano).

VS2015Example.Program

VS2015Example.Program + <> c

Więc co to jest VS2015Example.Program+<>c typ? Okazuje się, że jest to lambda w metodzie .Where(). Tak, to prawda, w jakiś sposób, że lokalna lambda jest eksponowana jako typ. Jeśli skomentuję numer .Where() w VS2015, to nie będę już otrzymywać drugiej linii.

Użyłem Beyond Compare, aby porównać dwa pliki .csproj, ale jedyne różnice to numer wersji VS, GUID projektu, nazwy domyślnej przestrzeni nazw i zespołu, a VS2015 ma odniesienie do System.Net .Http, że VS2013 nie.

Czy ktoś jeszcze to widział?

Czy ktoś ma wyjaśnienie, dlaczego zmienna lokalna zostałaby przedstawiona jako typ na poziomie zespołu?

Odpowiedz

29

Czy ktoś jeszcze to widział?

Tak, jest to spowodowane przez nowe zachowanie kompilatora do podnoszenia wyrażeń lambda.

Poprzednio, jeśli wyrażenie lambda nie przechwyciło żadnych zmiennych lokalnych, byłoby zbuforowane jako metoda statyczna w witrynie wywołania, co spowodowało, że zespół kompilatora musiał przeskoczyć niektóre obręcze, aby poprawnie wyrównać argumenty metody i parametr this. Nowe zachowanie w Roslyn polega na tym, że wszystkie wyrażenia lambda są przenoszone do klasy wyświetlania, w której delegat jest prezentowany jako metoda instancji w klasie wyświetlania, bez względu na to, czy przechwytuje jakiekolwiek zmienne lokalne.

Jeśli dekompilować metodę w Roslyn, widać to:

private static void Main(string[] args) 
{ 
    IEnumerable<Type> arg_33_0 = typeof(Program).Assembly.GetTypes(); 
    Func<Type, bool> arg_33_1; 
    if (arg_33_1 = Program.<>c.<>9__0_0 == null) 
    { 
     arg_33_1 = Program.<>c.<>9__0_0 = 
         new Func<Type, bool>(Program.<>c.<>9.<Main>b__0_0); 
    } 
    using (IEnumerator<Type> enumerator = arg_33_0.Where(arg_33_1).GetEnumerator()) 
    { 
     while (enumerator.MoveNext()) 
     { 
      Console.WriteLine(enumerator.Current.FullName); 
     } 
    } 
    Console.ReadKey(); 
} 

[CompilerGenerated] 
[Serializable] 
private sealed class <>c 
{ 
    public static readonly Program.<>c <>9; 
    public static Func<Type, bool> <>9__0_0; 
    static <>c() 
    { 
     // Note: this type is marked as 'beforefieldinit'. 
     Program.<>c.<>9 = new Program.<>c(); 
    } 
    internal bool <Main>b__0_0(Type t) 
    { 
     return !t.IsAbstract && t.IsClass; 
    } 
} 

Gdzie jest ze starego kompilatora, można zobaczyć:

[CompilerGenerated] 
private static Func<Type, bool> CS$<>9__CachedAnonymousMethodDelegate1; 

private static void Main(string[] args) 
{ 
    IEnumerable<Type> arg_34_0 = typeof(Program).Assembly.GetTypes(); 
    if (Program.CS$<>9__CachedAnonymousMethodDelegate1 == null) 
    { 
     Program.CS$<>9__CachedAnonymousMethodDelegate1 = 
          new Func<Type, bool>(Program.<Main>b__0); 
    } 
    IEnumerable<Type> types = 
       arg_34_0.Where(Program.CS$<>9__CachedAnonymousMethodDelegate1); 

    foreach (Type type in types) 
    { 
     Console.WriteLine(type.FullName); 
    } 
    Console.ReadKey(); 
} 

[CompilerGenerated] 
private static bool <Main>b__0(Type t) 
{ 
    return !t.IsAbstract && t.IsClass; 
} 

można uzyskać pożądany wynik filtrując obecnie klas, które mają atrybut CompilerGenerated dołączony do nich:

var types = typeof(Program) 
      .Assembly 
      .GetTypes() 
      .Where(t => !t.IsAbstract && 
         t.IsClass && 
         Attribute.GetCustomAttribute(
          t, typeof (CompilerGeneratedAttribute)) == null); 

dla więcej, zobacz moje pytanie Delegate caching behavior changes in Roslyn

+1

Dzięki za info. Wydaje się trochę przerażające, ponieważ wydaje się, że zmiana może spowodować wiele istniejącego kodu, który działa dobrze, aby nagle pokazać błędy. Przez lata straciłem rachubę ile razy napisałem kod, który wylicza na typy w zespole. Wygląda na to, że 'GetTypes()' powinien mieć przeciążenie, które pozwala deweloperowi jawnie określić, czy chce on zawierać typy generowane przez kompilator. –

+0

@CraigW. Powinieneś być całkiem łatwy do napisania metody rozszerzenia, ale całkowicie zgadzam się, że jest to potencjalnie przełomowa zmiana, ponieważ nawet przy metodzie rozszerzenia nie byłaby ona domyślnie wywoływana, może powinieneś zgłosić problem z zespołem Roslyn na github? –

+0

@Craig To nie jest przełomowa zmiana, to jest *** szczegóły dotyczące implementacji ***. Jeśli przechwyciłeś zmienną wewnątrz swojego delegata, zobaczysz to samo zachowanie. –