V3161. Comparing value type variables with 'ReferenceEquals' is incorrect because compared values will be boxed.
Анализатор обнаружил подозрительный вызов метода 'Object.ReferenceEquals': один или оба переданных аргумента могут иметь значимый тип. Так как метод принимает параметры типа 'Object', при передаче значимых типов будет произведена упаковка. Таким образом, в 'Object.ReferenceEquals' будет передана ссылка на объект, созданный в куче при упаковке. Так как полученная ссылка не будет равна никакой другой, из этого следует, что метод будет возвращать 'false' независимо от значения переданного аргумента.
Рассмотрим простой синтетический пример:
void SyntheticMethod(Point x, object a)
{
if (Object.ReferenceEquals(x, a))
....
}
В качестве аргументов в 'Object.ReferenceEquals' передаются 'x' и 'a'. Переменная 'x' имеет тип 'Point', следовательно, её значение будет упаковано при преобразовании в 'Object'. Таким образом, вызов 'Object.ReferenceEquals' всегда будет возвращать 'false' вне зависимости от того, какие аргументы переданы в 'SyntheticMethod'.
В качестве исправления можно заменить вызов метода на непосредственную проверку равенства значений:
void SyntheticMethod(Point x, object a)
{
if (a is Point pointA && pointA == x)
....
}
Правило также применяется и к аргументам, типы которых являются generic-параметрами. Если значения такого параметра не ограничены ссылочными типами, то анализатор будет выдавать предупреждение. Пример:
void SomeFunction<T>(T genericObject,
object someObject)
{
if (Object.ReferenceEquals(genericObject, someObject))
....
}
Здесь 'genericObject' может быть экземпляром как ссылочного, так и значимого типа. Для значимых типов 'Object.ReferenceEquals' всегда будет возвращать 'false'. Такое поведение может оказаться неожиданным и нежелательным. Чтобы быть уверенным в том, что в метод не будут передаваться объекты значимых типов, нужно добавить соответствующее ограничение параметра:
void SomeFunction<T>(T genericObject,
object someObject) where T : class
{
if (Object.ReferenceEquals(genericObject, someObject))
....
}
Теперь на месте параметра может быть только ссылочный тип, и анализатор не будет выдавать предупреждение.
Рассмотрим ситуацию, в которой параметр ограничен интерфейсом:
void SomeFunction<T>(T genericObject,
object someObject) where T : ICloneable
{
if (Object.ReferenceEquals(genericObject, someObject))
....
}
Анализатор выдаст предупреждение на данный код, так как объекты значимых типов могут реализовывать интерфейсы. Таким образом, ограничение 'where T : ICloneable' не защищает от вызова метода для структур, и результат такого вызова может оказаться неожиданным.
Отдельно рассмотрим одну особенность, связанную с nullable-типами. Они являются значимыми, следовательно, их преобразование в 'Object' приведёт к упаковке. Однако упаковка объектов таких типов производится особым образом. Если переменной присвоено значение, то при упаковке в кучу попадёт не объект nullable-типа, а само значение. Если же значение не задано, то при упаковке будет получена нулевая ссылка.
В следующем примере 'x' имеет тип 'Nullable<Point>'. Вполне возможно, что в результате упаковки 'x' будет возвращена нулевая ссылка. В таком случае вызов 'Object.ReferenceEquals' вернёт 'true', если аргумент 'a' также будет равен 'null'.
void SyntheticMethod(Point? x, object a)
{
if (Object.ReferenceEquals(x, a))
....
}
Тем не менее, на этот код будет выдано предупреждение. Использование такого способа для проверки двух элементов на равенство 'null' выглядит подозрительно. Уместнее будет проверять наличие значений непосредственным сравнением с 'null' или с помощью свойства 'HasValue':
void SyntheticMethod(Point? x, object a)
{
if (!x.HasValue && a == null)
....
}
Дополнительные материалы по теме особенностей nullable-типов:
- Хорошо ли вы помните nullable value types? Заглядываем "под капот"
- Nullable value types (C# reference)
- Boxing and Unboxing (C# Programming Guide)
Данная диагностика классифицируется как: