Почему bool Equal(R obj) класса записи является виртуальным методом

Из спецификации :

Тип записи реализует System.IEquatable<R> и включает в себя синтезировала строго типизированную перегрузку Equals(R? other), где R — это тип записи. Метод public, а метод virtual, если только тип записи — sealed.

Какова реальная цель объявления метода как virtual, когда мы не можем переопределить его с помощью пользовательского кода в производных классах, поскольку он автоматически реализуется компилятором? Что автоматически генерируемая компилятором реализация гарантирует хорошую работу записей, без чего иначе мы не смогли бы жить?

return Equals((object)other);

Записи могут передаваться по наследству.

Sinatr 02.08.2024 14:06

Поскольку в производной записи методу Equals может потребоваться включить дополнительные проверки, поэтому его необходимо переопределить.

DavidG 02.08.2024 14:07
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
2
73
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Записи могут быть унаследованы, и компилятор переопределит строго типизированный Equals в производной записи, поэтому так и должно быть virtual.

Учитывать

public record A {
    public string Foo { get; set; } = "";
}

public record B: A {
    public string Bar { get; set; } = "";
}

B наследует метод Equals(A) от A (конечно, в дополнение к собственному методу Equals(B)), который B должен переопределить, потому что в противном случае Equals(A) только сравнивает Foo, что неверно при сравнении двух экземпляров B.

Если Equals(A) не переопределено в B, приведенный ниже код выведет «True», потому что сравнивается только Foo, что весьма неожиданно, если вы спросите меня.

Compare(
    new B() { Foo = "a", Bar = "b" }, 
    new B() { Foo = "a", Bar = "c" }
);

void Compare(A a, A b) {
    // this calls the 'Equals(A)' overload!
    Console.WriteLine(a.Equals(b));
}

Я думаю, что вопрос ОП может заключаться в том, почему невозможно переопределить public virtual bool Equals(A? a) в B. Я на самом деле тоже задаюсь этим вопросом.

Pedro Costa 02.08.2024 14:21

@PedroCosta Я думаю, это та же причина, по которой вам не разрешено явно переопределять Equals(object). Нужно быть очень осторожным при реализации этих Equals методов, чтобы все они делали одно и то же. Если позволить программистам переопределить строго типизированный Equals, то будет сложнее совершать ошибки.

Sweeper 02.08.2024 14:30

@Sweeper Я думал, что это нужно для предотвращения чего-то подобного, но не смог найти официального обоснования того, почему это было неожиданно. Метод заботится об A, и часть A идентична между двумя B, поэтому они «равны». Когда это может стать проблемой?

Ivan Petrov 02.08.2024 14:57

это также не позволяет дочерним элементам A, имеющим одинаковые EqualityContract по отношению к A, не проверять равенство по ссылкам A, где a.Equals(b1) и a.Equals(b2) были бы истинными.

Ivan Petrov 02.08.2024 15:00

@IvanPetrov Контракт Equals не в этом. Вы же не ожидаете, что ((object)someString).Equals((object)someOtherString) будет сравнивать только ссылки на объекты, не так ли? Считаете ли вы, что выражение должно «заботиться только о части objectstring» и возвращает false только потому, что строки не являются одним и тем же экземпляром?

Sweeper 02.08.2024 15:09

@IvanPetrov Другим примером может быть общий набор вещей, происходящих от A. class SomeCollection<T> where T: A, и вы создаете SomeCollection<B>, добавляете в него несколько B и спрашиваете, содержит ли он какой-нибудь другой экземпляр B. Разве не было бы странно, если бы учитывались только свойства, объявленные в A? Теперь аргумент «метод заботится об А, а часть А идентична» неприменим.

Sweeper 02.08.2024 15:32

@Sweeper, где указан контракт Equals(T) на records? Потому что Equals работает по-разному для ValueType, string и object. Мы можем сделать вывод о том, что представляет собой контракт, из экспериментов, но я не видел, чтобы контракт был сформулирован явно, и что наличие Equals(Aother) также эффективно virtual sealed обеспечивает его соблюдение.

Ivan Petrov 02.08.2024 16:33

Давайте продолжим обсуждение в чате.

Sweeper 02.08.2024 16:39
Ответ принят как подходящий

Ответ Sweeper синхронизирован с текущими официальными документами, которые описывают равенство значений:

Для типов с модификатором записи (класс записи, структура записи и структура записи только для чтения), два объекта равны, если они имеют одно и то же введите и сохраните одни и те же значения.

это однако

Если вы не переопределите или не замените методы равенства, тип, который вы Объявление определяет, как определяется равенство:

Итак, слегка изменив пример Sweeper, переопределив виртуальное свойство EqualityContract:

void Main() {
    Compare(
    new A() { Foo = "a"},
    new B() { Foo = "a", Bar = "c" }
);
}

public record A {
    public string Foo { get; set; } = "";
}

public record B : A {
    public string Bar { get; set; } = "";
    protected override Type EqualityContract => base.EqualityContract;
}

void Compare(A a, A b) {
    Console.WriteLine(a.Equals(b)); // True
}

Здесь между двумя объектами оценивается только часть Foo и возвращает true. Однако b.Equals(a) возвращает false, что немного сбивает с толку, как показано (и подробно обсуждается) в вопросах this и this.

Моя лучшая попытка концептуализировать эту функцию заключается в том, что она просто позволяет нам расширить концепцию одного и того же типа (инвариантного) до типа, которому можно назначать (ковариантный).

Например:

public record A(string FirstName) {}
public record B(string FirstName, string LastName) : A(FirstName) {
    protected override Type EqualityContract => base.EqualityContract;
}
public record C(string FirstName, string LastName, int Age) : B(FirstName, LastName) {
    protected override Type EqualityContract => base.EqualityContract;
}

var aRecord = new A("John");
var bRecord = new B("John", "Doe");
var cRecord = new C("John", "Doe", 33);


aRecord.Equals(cRecord).Dump(); // True -> C is assignable to A
aRecord.Equals(bRecord).Dump(); // True -> B is assignable to A
bRecord.Equals(cRecord).Dump(); // True -> C is assignable to B

cRecord.Equals(aRecord).Dump(); // False -> A is NOT assignable to C

Эта форма ковариации работает только с типом реального времени выполнения объекта, а не с типом ссылки на переменную, т.е.

A cRecord = ...
cRecord.Equals(aRecord).Dump() // will evalute to false

По моему мнению, это связано с исходной реализацией (скоро ниже), которая опиралась на виртуальную отправку object.Equals.

Самое каноническое объяснение того, что EqualityContract должно быть включено, которое я смог найти, датируется марта 2020 , когда набранный Equals(R obj еще еще не был решен и они использовали object.Equals для примеров:

public abstract class Person // Root of hierarchy with value equality
{
    public string Name { get; set; }
    protected virtual Type EqualityContract => typeof(Person);
    public override bool Equals(object other) =>
        other is Person that
        && object.Equals(this.EqualityContract, that.EqualityContract)
        && object.Equals(this.Name, that.Name);
}

public class Student : Person // derived class
{
    public int ID { get; set; }
    protected override Type EqualityContract => typeof(Student);
    public override bool Equals(object other) =>
        base.Equals(other) // checks EqualityContract and Name
        
        && other is Student that // if other is just Person won't work
        
        && object.Equals(this.ID, that.ID);
}

По этому вопросу велась некоторая дискуссия о том, почему бы не реализовать проверку типа в базовом классе с помощью подхода, аналогичного this.GetType()==other.GetType(), который помог бы предотвратить оценку базовым классом ТОЛЬКО своего состояния по экземпляру производного класса и ошибочное возвращение true. Основной ответ был:

Некоторые люди считают, что это нишевый сценарий, и компилятор должен просто сгенерируйте проверку typeof(...) напрямую. Я не знаю. Мне нравится решение всей проблемы равенства иерархий. Иметь два значения согласен, что у них одинаковый договор о равенстве, кажется, это полностью обращается к симметрии, соблюдая при этом выбор производных классов («Я хочу быть похожим на своего родителя» или «Я сам по себе». Я думаю этот подход (первоначально предложенный @gafter давным-давно) супер элегантный.

В июне 2020 они разработали напечатанные Equals(R obj) сценарии наследования, которые функционально ведут себя так же, как сегодня. Я не смог найти во всех записях встречи, почему они решили сделать методы эффективными sealed virtual (вы не можете легально переопределить методы в производных классах).

Если бы у нас не было функции EqualityContract и мы просто сравнивали точный тип в корневой записи Equals (нет пользовательской опции для назначаемости/ковариации), эти методы на самом деле могли бы быть просто методами экземпляра, а не виртуальными. Используя пример с A, B, C сверху.

A aRecord = new A...
A bRecord = new B...

bRecord.Equals(aRecord); 
// this would use the A.Equals which could 
// have just checked for exact type match (between this and other) and returned false.

Но с помощью переопределяемой проверки EqualityContract в корневом элементе A.Equals мы могли бы получить истинное значение. A не может быть присвоено B, как B было присвоено A из первого примера, так что это больше не является исходным (уже сбивающим с толку некоторых) ковариантным решением. Поэтому они должны были предотвратить это.

Они сделали так, чтобы метод был virtual, чтобы вызвать виртуальную отправку, где они могли бы перенаправить объект проверки типа obj.Equals, чтобы предотвратить такое поведение:

// B's override of A.Equals that we cannot generate ourselves
public sealed override bool Equals(A other)
{
    return Equals((object)other); // let's say we have A object
}

// object.Equals that we cannot generate ourselves
public override bool Equals(object obj)
{
    // effectively preventing us from comparing
    // objects that are not assignable to B from
    // comparing to B at all

    return Equals(obj as B); // will evaluate to null -> false
}

Другие вопросы по теме

Похожие вопросы

Каковы риски запуска экземпляра IHost и экземпляра WebApplication в одном процессе .NET?
Запись в один и тот же файл с помощью двух операторов using и двух операторов записи
Оллама ничего не сохраняет в контексте
Blazor EditForm: кнопка отключения приводит к тому, что все нетронутые поля помечаются как недействительные
Почему я не могу использовать DLL .NET 8 из моего приложения .NET Framework 4.7.2 Windows Forms?
.NET 8: MemoryCache SlidingExpiration без доступа
Не удалось загрузить файл или сборку «System.Diagnostics.DiagnosticSource», версия = 6.0.0.0. Система не может найти указанный файл
Зачем использовать структуру для хранения констант, а не статический класс?
Невозможно получить доступ к наблюдаемому свойству в модели представления из соответствующего файла .xaml представления — .NET MAUI MVVM
Устойчивые функции Azure не вызываются при использовании с триггером Q с использованием управляемого удостоверения