在Visual Studio 2015中,Assembly.GetTypes()的行为发生了变化。

25
我昨天在Visual Studio 2015中打开了我们的解决方案,发现一些单元测试(在Visual Studio 2013中运行良好)开始失败。进一步深入研究,我发现这是因为在程序集上调用GetTypes()返回不同的结果。我已经能够创建一个非常简单的测试用例来说明这个问题。
在Visual Studio 2013和2015中,我都使用.NET Framework 4.5.2创建了一个新的控制台应用程序。我在这两个项目中放置了以下代码。
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();
    }
}

当我在Visual Studio 2013中运行时,我得到了如下输出(如预期)。
“VS2013Example.Program”
当我在Visual Studio 2015中运行时,我得到了如下输出(不是预期的)。
“VS2015Example.Program” “VS2015Example.Program+<>c”
那么这个“VS2015Example.Program+<>c”类型是什么?原来它是.Where()方法内部的lambda表达式。是的,没错,某种方式将该局部lambda表达式公开为类型。如果我在VS2015中注释掉.Where(),则不再会出现第二行。
我使用Beyond Compare比较了两个.csproj文件,但唯一的区别是VS版本号、项目GUID、默认命名空间和程序集的名称,以及VS2015引用了VS2013没有的System.Net.Http。
有其他人看到过这个问题吗?
有人能解释一下为什么一个局部变量会被公开为程序集级别的类型吗?
1个回答

29
有人看到过这个问题吗?是的,这是由于新编译器行为提升Lambda表达式所致。以前,如果Lambda表达式没有捕获任何局部变量,它将作为静态方法缓存到调用站点,这使得编译器团队需要跳过一些障碍才能正确对齐方法参数和“this”参数。Roslyn中的新行为是将所有Lambda表达式提升到显示类中,在显示类中将委托公开为实例方法,而不管它是否捕获任何局部变量。如果在Roslyn中反编译您的方法,您会看到这个:
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;
    }
}

在旧编译器中,你会看到这样的内容:

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

您可以通过过滤掉带有CompilerGenerated属性的类来获得所需的结果:

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

更多内容请参见我的问题Roslyn中委托缓存行为的更改


1
谢谢提供信息。这似乎有点可怕,因为它感觉像是一种可能会导致许多现有代码突然出现错误的变化。多年来,我已经数不清写过多少次枚举程序集中类型的代码了。感觉GetTypes()应该有一个重载,让开发人员明确地声明他们是否想要包括编译器生成的类型。 - Craig W.
@CraigW。应该很容易为此编写扩展方法,但我完全同意这可能会导致破坏性变更,因为即使有了扩展方法,它也不会被默认调用,也许你应该在GitHub上向Roslyn团队提出问题? - Ron Beyer
@Craig 这不是一个破坏性的变化,而是一个实现细节。如果你在委托中捕获了一个变量,你会看到相同的行为。 - Yuval Itzchakov
4
我们必须就这是否是破坏性变更达成不同的看法。它破坏了我的代码。 :-) - Craig W.
1
@CraigW 编译器神奇地暴露出以前不存在的新类型,不仅仅是一种简单的实现细节。 这并不完全正确。编译器现在在调用点生成一个显示类而不是静态方法这一事实,属于一种实现细节。如果您的代码涉及到某个本地值的clojure,您在编写代码时可能更早地发现此错误。当您使用Assembly.GetTypes时,您确实希望看到程序集中的所有类,即使它们是由编译器生成的。 - Yuval Itzchakov
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接