V3161. Comparing value type variables with 'ReferenceEquals' is incorrect because compared values will be boxed.
The analyzer has detected a suspicious call of the 'Object.ReferenceEquals' method: either one or both of the passed arguments are of the value type. Since the method's parameters are of type 'Object', arguments of the value type will be boxed when passed to the method. As a result of such boxing, an object will be created on the heap and a reference to that object will be passed to the 'Object.ReferenceEquals' method. Since this reference is not equal to any other reference, the method will return 'false' regardless of the passed argument's value.
Consider a simple synthetic example:
void SyntheticMethod(Point x, object a)
{
if (Object.ReferenceEquals(x, a))
....
}
The variables 'x' and 'a' are passed to the 'Object.ReferenceEquals' method. Since 'x' is of type 'Point', its value will be boxed as it gets cast to type 'Object'. The call to 'Object.ReferenceEquals' will, therefore, always return 'false' no matter what arguments were passed to 'SyntheticMethod'.
This issue can be fixed by replacing the method call with a direct comparison of the values:
void SyntheticMethod(Point x, object a)
{
if (a is Point pointA && pointA == x)
....
}
This rule is also applied to arguments whose types are generic parameters. Unless values for such a parameter are limited to reference types only, the analyzer will issue the warning. For example:
void SomeFunction<T>(T genericObject,
object someObject)
{
if (Object.ReferenceEquals(genericObject, someObject))
....
}
Here, 'genericObject' could be an instance of both a reference type and a value type. 'Object.ReferenceEquals' will always return 'false' for value types. Such behavior might be unexpected and unwanted. To make sure that no objects of value types can be passed to the method, limit the parameter accordingly:
void SomeFunction<T>(T genericObject,
object someObject) where T : class
{
if (Object.ReferenceEquals(genericObject, someObject))
....
}
Now only a reference type can be substituted for the parameter, so the analyzer will no longer issue the warning.
In the following example, the parameter is limited to an interface:
void SomeFunction<T>(T genericObject,
object someObject) where T : ICloneable
{
if (Object.ReferenceEquals(genericObject, someObject))
....
}
The analyzer will report this code since value-type objects can implement interfaces. Thus, the limitation 'where T : ICloneable' does not protect the method from being invoked on structures, and such a call may lead to unpredictable results.
There is one specific point about boxing nullable types that we need to discuss separately. They are of the value types, so they will be boxed when they are cast to 'Object'. However, objects of these types are boxed in their own special way. If such a variable has a value, the boxing will allocate the value itself on the heap, rather than the nullable-type object. If the variable has no value, the boxing will return a null reference.
In the following example, 'x' is of type 'Nullable<Point>'. When 'x' is boxed, you are quite possible to get a null reference. In that case, the call of 'Object.ReferenceEquals' will return 'true' if the 'a' argument is 'null' too.
void SyntheticMethod(Point? x, object a)
{
if (Object.ReferenceEquals(x, a))
....
}
However, this code will still trigger the warning because testing two items against 'null' in a way like that is suspicious. A better way to check whether the variables have values is to directly compare them with 'null' or to use the 'HasValue' property:
void SyntheticMethod(Point? x, object a)
{
if (!x.HasValue && a == null)
....
}
More on specifics of nullable types:
- Check how you remember nullable value types. Let's peek under the hood
- Nullable value types (C# reference)
- Boxing and Unboxing (C# Programming Guide)
This diagnostic is classified as: