为什么将2个.NET框架类相互比较会产生堆栈溢出异常?

Dev*_*dse 10 c# reflection json.net

问题

我正在创建一个应用程序.在这个应用程序中,我正在使用序列化Func.这种方式不知何故使我的应用程序崩溃了.

崩溃毫无例外让我对wtf感到好奇,所以我做了一些深度潜水,经过一些挖掘后终于发现Newtonsoft.Json中的某个地方List.Contains正在发生,然后对2个属性执行等于检查.

显然在此等于检查会导致无限循环,从而导致堆栈溢出异常.

仅使用C#重现问题

Expression<Func<string, int>> expr = (t) => t.Length;
Func<string, int> exprCompiled = expr.Compile();

var aa = exprCompiled.Method.Module;
var bb = exprCompiled.Method.Module.Assembly;

//This code results in either an infinite loop or a Stackoverflow Exception
var tempresult = aa.Equals(bb);

Console.WriteLine("This code is never executed");
Run Code Online (Sandbox Code Playgroud)

使用Newtonsoft.Json重现问题

Expression<Func<string, int>> expr = (t) => t.Length;
Func<string, int> exprCompiled = expr.Compile();

//This code results in either an infinite loop or a Stackoverflow Exception
var res = JsonConvert.SerializeObject(exprCompiled);

Console.WriteLine("This code is never executed");
Run Code Online (Sandbox Code Playgroud)

实际的潜在问题

进一步深入研究.NET框架的工作原理我认为问题在于InternalAssemblyBuilder内部类和InternalModuleBuilder内部类的实现.它们都有一个Equals方法覆盖,如下所示:

public override bool Equals(object obj)
{
    if (obj == null)
    {
        return false;
    }
    if (obj is InternalAssemblyBuilder)
    {
        return this == obj;
    }
    return obj.Equals(this);
}
Run Code Online (Sandbox Code Playgroud)

我认为它应该是这样的:

public override bool Equals(object obj)
{
    if (obj == null)
    {
        return false;
    }
    if (obj is InternalAssemblyBuilder)
    {
        return this == obj;
    }
    return base.Equals(this); //changed obj to base
}
Run Code Online (Sandbox Code Playgroud)

dbc*_*dbc 6

正如您的问题的实际根本问题以及NineBerry评论中所述,微软的实施InternalAssemblyBuilder.Equals(object)并且InternalModuleBuilder.Equals(object)似乎已被打破.具体地,在检查类型InternalAssemblyBuilder的对象和类型的对象之间的相等性的情况下,InternalModuleBuilder将发生无限递归.

要解决此问题,您可以设置自定义IEqualityComparerJsonSerializer.SettingsEqualityComparer,替代品的合理实现Equals()这些类型.以下是使用引用相等性的一个这样的示例:

public class CustomJsonEqualityComparer : IEqualityComparer
{
    public static readonly CustomJsonEqualityComparer Instance = new CustomJsonEqualityComparer();

    // Use ImmutableHashSet in later .net versions
    static readonly HashSet<string> naughtyTypes = new HashSet<string>
    {
        "System.Reflection.Emit.InternalAssemblyBuilder",
        "System.Reflection.Emit.InternalModuleBuilder"
    };

    static readonly IEqualityComparer baseComparer = EqualityComparer<object>.Default;

    static bool HasBrokenEquals(Type type)
    {
        return naughtyTypes.Contains(type.FullName);
    }

    #region IEqualityComparer Members

    public bool Equals(object x, object y)
    {
        // Check reference equality
        if ((object)x == y)
            return true;
        // Check null
        else if ((object)x == null || (object)y == null)
            return false;

        var xType = x.GetType();
        if (xType != y.GetType())
            // Types should be identical.
            // Note this check alone might be sufficient to fix the problem.
            return false;

        if (xType.IsClass && !xType.IsPrimitive) // IsPrimitive check for performance
        {
            if (HasBrokenEquals(xType))
            {
                // These naughty types should ONLY be compared via reference equality -- which we have already done.
                // So return false
                return false;
            }
        }
        return baseComparer.Equals(x, y);
    }

    public int GetHashCode(object obj)
    {
        return baseComparer.GetHashCode(obj);
    }

    #endregion
}
Run Code Online (Sandbox Code Playgroud)

然后你会按如下方式使用它:

var settings = new JsonSerializerSettings
{
    EqualityComparer = CustomJsonEqualityComparer.Instance,
};
var json = JsonConvert.SerializeObject(exprCompiled, settings);
Run Code Online (Sandbox Code Playgroud)

笔记:

  • CustomJsonEqualityComparer.HasBrokenEquals()如果其他System.Reflection.Emit类型具有类似的破坏实现,您可能需要调整Equals().

  • 确保两个传入对象具有相同的System.Type值可能就足够了GetType(),因为Equals()到目前为止发现的损坏方法仅在比较两种不同类型的情况下溢出堆栈,并且两者具有相同的错误.

  • 虽然我能够重现无限递归并验证它是否已使用一些模型对象进行修复,但我无法确认Json.NET是否可以实际序列化您的Func<string, int> exprCompiled.