24

Ich öffnete unsere Lösung in Visual Studio 2015 gestern und einige unserer Unit-Tests (die in Visual Studio 2013 einwandfrei funktionierte) starten fehlgeschlagen. Digger Tiefer ich entdeckte, dass es war, weil das Anrufen GetTypes() auf einer Versammlung unterschiedliche Ergebnisse zurückgab. Ich konnte einen sehr einfachen Testfall erstellen, um es zu veranschaulichen.Verhalten von Assembly.GetTypes() in Visual Studio 2015 geändert

In Visual Studio 2013 und 2015 habe ich eine neue Konsolenanwendung mit .NET Framework 4.5.2 erstellt. Ich habe den folgenden Code in beide Projekte eingefügt.

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(); 
    } 
} 

Wenn ich in Visual Studio 2013 ausführen, erhalte ich (wie erwartet) die folgende Ausgabe.

VS2013Example.Program

Als ich in Visual Studio 2015 betreibe ich die folgende Ausgabe erhalten (nicht wie erwartet).

VS2015Example.Program

VS2015Example.Program + <> c

Also, was ist, dass VS2015Example.Program+<>c Typ? Stellt sich heraus, es ist das Lambda innerhalb der .Where() Methode. Ja, das stimmt, irgendwie wird das lokale Lambda als ein Typ herausgestellt. Wenn ich die .Where() in VS2015 auskommentiere, dann bekomme ich diese zweite Zeile nicht mehr.

Ich habe Beyond Compare verwendet, um die beiden .csproj-Dateien zu vergleichen, aber die einzigen Unterschiede sind die VS-Versionsnummer, die Projekt-GUID, die Namen des Standardnamespace und der Assembly, und die VS2015 hatte einen Verweis auf System.Net .Http, dass der VS2013 nicht.

Hat jemand anderes dies gesehen?

Hat jemand eine Erklärung, warum eine lokale Variable als ein Typ auf der Assembly-Ebene exponiert würde?

Antwort

29

Hat jemand anderes das gesehen?

Ja, dies wird durch das neue Compiler-Verhalten zum Anheben von Lambda-Ausdrücken verursacht.

Wenn zuvor ein Lambda-Ausdruck keine lokalen Variablen erfasst hat, wird er als statische Methode an der Aufruf-Site zwischengespeichert, wodurch das Compiler-Team einige Umrisse springen muss, um die Methodenargumente ordnungsgemäß auszurichten der this Parameter. Das neue Verhalten in Roslyn besteht darin, dass alle Lambda-Ausdrücke in eine Anzeigeklasse gehoben werden, in der der Delegat als Instanzmethode in der Anzeigeklasse angezeigt wird, unabhängig davon, ob er lokale Variablen erfasst.

Wenn Sie Ihre Methode in Roslyn dekompilieren, sehen Sie dies:

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; 
    } 
} 

Wo mit dem alten Compiler ist, dann würden Sie sehen:

[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; 
} 

Sie können das gewünschte Ergebnis durch Filterung aus Klassen, die das Attribut CompilerGenerated mit ihnen verbunden haben:

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

Für mehr, siehe meine Frage Delegate caching behavior changes in Roslyn

+1

Danke für die Info. Scheint ein wenig beängstigend, weil es sich wie eine Veränderung anfühlt, die wahrscheinlich viel existierenden Code verursacht, der gut funktioniert hat, um plötzlich Bugs zu zeigen. Im Laufe der Jahre habe ich nicht mehr gezählt, wie oft ich Code geschrieben habe, der die Typen in einer Baugruppe aufzählt. Empfindungen wie 'GetTypes()' sollten vielleicht eine Überladung haben, die den Entwickler explizit angeben lässt, ob sie die vom Compiler generierten Typen enthalten wollen. –

+0

@CraigW. Sollte es ziemlich einfach sein, eine Erweiterungsmethode dafür zu schreiben, aber ich stimme völlig zu, dass es eine potentielle Bruchänderung ist, denn selbst mit einer Erweiterungsmethode würde es nicht standardmäßig aufgerufen werden, vielleicht sollten Sie ein Problem mit dem Roslyn-Team auf GitHub einreichen? –

+0

@Craig Dies ist keine brechende Änderung, das ist ein *** Implementierungsdetail ***. Wenn Sie eine Variable in Ihrem Delegaten erfasst hätten, würden Sie das gleiche Verhalten sehen. –