Reputation: 2109
I was working on a project and I found that the operator I used and the one I declared were not equal.
I made a minimum reproducible example:
var tree = CSharpSyntaxTree.ParseText(@"
bool a = 3 > 5;
namespace System{
public struct Int32
{
public static extern bool operator > (int a, int b);
}
public struct Boolean { }
}");
var compilation = CSharpCompilation.Create("bla").AddSyntaxTrees(tree);
var model = compilation.GetSemanticModel(tree);
var usedSymbol = model.GetSymbolInfo(tree.GetRoot().DescendantNodes().OfType<BinaryExpressionSyntax>().Single()).Symbol;
var declaredSymbol = model.GetDeclaredSymbol(tree.GetRoot().DescendantNodes().OfType<OperatorDeclarationSyntax>().Single());
Console.WriteLine(
$"{declaredSymbol} and {usedSymbol} are {(declaredSymbol.Equals(usedSymbol) ? "" : "not ")}equal.");
// int.operator >(int, int) and int.operator >(int, int) are not equal.
Why aren't these operators that seem the same showing that they are equal?
Upvotes: 0
Views: 91
Reputation: 1324
I modified your code, and using Reflection together with a peek at the Roslyn source, have found that usedSymbol
and declaredSymbol
end up as two distinct Symbol types.
var tree = CSharpSyntaxTree.ParseText(@"
bool a = 3 > 5;
namespace System{
public struct Int32
{
public static extern bool operator > (int a, int b);
}
public struct Boolean { }
}");
var compilation = CSharpCompilation.Create("bla").AddSyntaxTrees(tree);
var model = compilation.GetSemanticModel(tree);
var usedSymbol = model.GetSymbolInfo(tree.GetRoot().DescendantNodes().OfType<BinaryExpressionSyntax>().Single()).Symbol;
var declaredSymbol = model.GetDeclaredSymbol(tree.GetRoot().DescendantNodes().OfType<OperatorDeclarationSyntax>().Single());
Type used = usedSymbol.GetType();
Type declared = declaredSymbol.GetType();
var usedUnderlying = used.GetField("_underlying", BindingFlags.NonPublic | BindingFlags.Instance);
var usedUnderlyingValue = usedUnderlying.GetValue(usedSymbol);
var declaredUnderlying = declared.GetField("_underlying", BindingFlags.NonPublic | BindingFlags.Instance);
var declaredUnderlyingValue = declaredUnderlying.GetValue(declaredSymbol);
Type usedSymbolType = usedUnderlyingValue.GetType(); //SynthesizedIntrinsicOperatorSymbol
Type declaredSymbolType = declaredUnderlyingValue.GetType(); //SourceUserDefinedOperatorSymbol
Console.WriteLine(usedSymbolType.ToString());
Console.WriteLine(declaredSymbolType.ToString());
Console.WriteLine(
$"{declaredSymbol} and {usedSymbol} are {(declaredSymbol.Equals(usedSymbol) ? "" : "not ")}equal.");
The types for the two representations of the symbol do not match. One is SynthesizedIntrinsicOperatorSymbol, and the other is SourceUserDefinedOperatorSymbol. Ultimately, this is why equality doesn't work - it seems to not have been implemented for these two types.
For example, equality for SynthesizedIntrinsicOperatorSymbol does a type check, which would fail in this use case:
public override bool Equals(Symbol obj, TypeCompareKind compareKind)
{
if (obj == (object)this)
{
return true;
}
var other = obj as SynthesizedIntrinsicOperatorSymbol;
if ((object)other == null)
{
return false;
}
if (_isCheckedBuiltin == other._isCheckedBuiltin &&
_parameters.Length == other._parameters.Length &&
string.Equals(_name, other._name, StringComparison.Ordinal) &&
TypeSymbol.Equals(_containingType, other._containingType, compareKind) &&
TypeSymbol.Equals(_returnType, other._returnType, compareKind))
{
for (int i = 0; i < _parameters.Length; i++)
{
if (!TypeSymbol.Equals(_parameters[i].Type, other._parameters[i].Type, compareKind))
{
return false;
}
}
return true;
}
return false;
}
Looking into the other type, SourceUserDefinedOperatorSymbol, reveals that equality is implemented on a base class many layers deep: Symbols.MethodSymbol. Nothing in the inheritance chain for SourceUserDefinedOperatorSymbol overrides equality and implements a special equality check.
In looking at the source for MethodSymbol
, it does not override Object.Equals(object)
. (It does override a related method; more on that later.)
MethodSymbol
is derived from Symbol
. The source of Symbol
shows that it does override Object.Equals(object)
, which in turn calls another Equals function. Note the implementation and the comments:
public sealed override bool Equals(object obj)
{
return this.Equals(obj as Symbol, SymbolEqualityComparer.Default.CompareKind);
}
// By default we don't consider the compareKind, and do reference equality. This can be overridden.
public virtual bool Equals(Symbol other, TypeCompareKind compareKind)
{
return (object)this == other;
}
So it seems this class just returns reference equality by design.
The Equals(Symbol, TypeCompareKind)
method is virtual
, and the MethodSymbol
class overrides it, but only to check specific types. Because nothing in the inheritance chain for this type (SourceUserDefinedOperatorSymbol) overrides the equality methods, your code would still end up calling the base
version that uses reference equality:
public override bool Equals(Symbol other, TypeCompareKind compareKind)
{
if (other is SubstitutedMethodSymbol sms)
{
return sms.Equals(this, compareKind);
}
if (other is NativeIntegerMethodSymbol nms)
{
return nms.Equals(this, compareKind);
}
return base.Equals(other, compareKind);
}
Upvotes: 1