>
>
>
V3161. Comparing value type variables w…


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-типов:

Данная диагностика классифицируется как: