Из спецификации :
Тип записи реализует
System.IEquatable<R>
и включает в себя синтезировала строго типизированную перегрузкуEquals(R? other)
, гдеR
— это тип записи. Методpublic
, а методvirtual
, если только тип записи —sealed
.
Какова реальная цель объявления метода как virtual
, когда мы не можем переопределить его с помощью пользовательского кода в производных классах, поскольку он автоматически реализуется компилятором? Что автоматически генерируемая компилятором реализация гарантирует хорошую работу записей, без чего иначе мы не смогли бы жить?
return Equals((object)other);
Поскольку в производной записи методу Equals
может потребоваться включить дополнительные проверки, поэтому его необходимо переопределить.
Записи могут быть унаследованы, и компилятор переопределит строго типизированный 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
. Я на самом деле тоже задаюсь этим вопросом.
@PedroCosta Я думаю, это та же причина, по которой вам не разрешено явно переопределять Equals(object)
. Нужно быть очень осторожным при реализации этих Equals
методов, чтобы все они делали одно и то же. Если позволить программистам переопределить строго типизированный Equals
, то будет сложнее совершать ошибки.
@Sweeper Я думал, что это нужно для предотвращения чего-то подобного, но не смог найти официального обоснования того, почему это было неожиданно. Метод заботится об A, и часть A идентична между двумя B, поэтому они «равны». Когда это может стать проблемой?
это также не позволяет дочерним элементам A
, имеющим одинаковые EqualityContract
по отношению к A, не проверять равенство по ссылкам A, где a.Equals(b1) и a.Equals(b2) были бы истинными.
@IvanPetrov Контракт Equals
не в этом. Вы же не ожидаете, что ((object)someString).Equals((object)someOtherString)
будет сравнивать только ссылки на объекты, не так ли? Считаете ли вы, что выражение должно «заботиться только о части object
string
» и возвращает false только потому, что строки не являются одним и тем же экземпляром?
@IvanPetrov Другим примером может быть общий набор вещей, происходящих от A
. class SomeCollection<T> where T: A
, и вы создаете SomeCollection<B>
, добавляете в него несколько B
и спрашиваете, содержит ли он какой-нибудь другой экземпляр B
. Разве не было бы странно, если бы учитывались только свойства, объявленные в A
? Теперь аргумент «метод заботится об А, а часть А идентична» неприменим.
@Sweeper, где указан контракт Equals(T)
на records
? Потому что Equals
работает по-разному для ValueType
, string
и object
. Мы можем сделать вывод о том, что представляет собой контракт, из экспериментов, но я не видел, чтобы контракт был сформулирован явно, и что наличие Equals(Aother) также эффективно virtual sealed
обеспечивает его соблюдение.
Давайте продолжим обсуждение в чате.
Ответ 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
}
Записи могут передаваться по наследству.