IEquatable<string> не работает со статическим методом Equals

Я реализовал класс NonEmptyString, который не позволяет создавать, если он не пуст. Я реализовал этот класс IEquatable<NonEmptyString> и IEquatable<string>. У меня есть переопределения для Equals(object obj), Equals(NonEmptyString other), Equals(string other) и GetHashCode(). Затем я написал несколько тестов и увидел, что практически все работает. За исключением 1 случая, когда статический метод Equals вызывается со строковым параметром, являющимся первым параметром. Смотрите эту строку здесь.

string text = "ASDF123";
NonEmptyString nonEmptyString = NonEmptyString.CreateUnsafe("ASDF123");
Assert.True(text == nonEmptyString);
Assert.True(nonEmptyString == text);
Assert.True(text.Equals(nonEmptyString)); // This one returns true as expected.
Assert.True(nonEmptyString.Equals(text));
Assert.True(Equals(text, nonEmptyString)); //This is the only one that doesn't work.
Assert.True(Equals(nonEmptyString, text));

Мне интересно, почему это должно быть так - когда я смотрю на реализацию метода Equals для объекта, он вызывает виртуальный метод Equals(object obj). Так что, если этот метод возвращает false, то я ожидаю, что то же самое должно произойти только для text.Equals(nonEmptyString), но это работает. Это реализация статического Equals, которую я вижу, когда вхожу в вызов.

public static bool Equals(object? objA, object? objB)
{
    if (objA == objB)
    {
        return true;
    }
    if (objA == null || objB == null)
    {
        return false;
    }
    return objA.Equals(objB);
}

Я даже пытался таким образом переопределить операторы == для сравнения строки с NonEmptyString (на самом деле я не ожидал, что это поможет, но попробовать стоило)

public static bool operator ==(string obj1, NonEmptyString obj2)
public static bool operator !=(string obj1, NonEmptyString obj2)
public static bool operator ==(NonEmptyString obj1, string  obj2)
public static bool operator !=(NonEmptyString obj1, string obj2)

Могу ли я что-нибудь сделать, чтобы это сработало? Ожидается ли, что это не должно работать? Это ошибка в .NET?

Вот основная реализация (я удалил из нее второстепенные части).

public sealed class NonEmptyString : IEquatable<string>, IEquatable<NonEmptyString>
{
    private NonEmptyString(string value)
    {
        Value = value;
    }

    public string Value { get; }

    public static NonEmptyString CreateUnsafe(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            throw new ArgumentException("You cannot create NonEmptyString from whitespace, empty string or null.");
        }

        return new NonEmptyString(value);
    }

    public override int GetHashCode()
    {
        return Value.GetHashCode();
    }

    public override bool Equals(object obj)
    {
        return ReferenceEquals(this, obj) ||
               obj is NonEmptyString otherNonEmpty && Equals(otherNonEmpty) ||
               obj is string otherString && Equals(otherString);
    }

    public bool Equals(string other)
    {
        return Value.Equals(other);
    }

    public bool Equals(NonEmptyString other)
    {
        return Value.Equals(other?.Value);
    }

    public override string ToString()
    {
        return Value;
    }
}

Вы должны предоставить (соответствующий) код для класса. Вы также должны предоставить тестовый код, который не работает, как код, а не просто как ссылку.

jmcilhinney 21.08.2023 06:18

Не показывайте код в виде изображения. Мы не можем запускать изображения. Пожалуйста, опубликуйте полный минимальный воспроизводимый пример, чтобы мы могли запустить ваш код.

Enigmativity 21.08.2023 06:20

Спасибо, ребята, я обновил вопрос.

Pavel Kalandra 21.08.2023 06:44
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
3
60
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Я думаю, это потому, что Equals(object1, object2) это то же самое, что и вызов object1.ReferenceEquals(object2), а ReferenceEquals() нельзя переопределить. То есть: он пытается использовать метод stringReferenceEquals по умолчанию для сравнения вашего объекта класса со строкой, а не методы сравнения, которые вы определили для доступа к его свойству Value.

Я тоже думал об этом, но когда я вхожу в вызов статического Equals, он в конечном итоге вызывает objA.Equals(objB) - теперь я расширил вопрос с этим.

Pavel Kalandra 21.08.2023 07:17

Интерфейс IEquatable в C# позволяет сравнивать два объекта одного типа на предмет равенства. Обычно он используется для переопределения метода Equals для пользовательского класса.

Однако IEquatable не работает со статическим Equals методом. Причина этого в том, что IEquatable требует реализации метода Equals на уровне экземпляра, то есть он сравнивает текущий объект с другим объектом того же типа.

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

Чтобы IEquatable работал правильно, вам необходимо реализовать метод Equals на уровне экземпляра в классе, реализующем интерфейс. Например:

public class MyClass : IEquatable<MyClass>
{
    public string Property { get; set; }

    public bool Equals(MyClass other)
    {
        if (other == null)
            return false;

        return Property == other.Property;
    }

    public override bool Equals(object obj)
    {
        if (obj == null || !(obj is MyClass))
            return false;

        return Equals((MyClass)obj);
    }

    public override int GetHashCode()
    {
        return Property.GetHashCode();
    }
}

Затем вы можете использовать интерфейс IEquatable для сравнения экземпляров MyClass с помощью метода Equals:

MyClass obj1 = new MyClass { Property = "Test" };
MyClass obj2 = new MyClass { Property = "Test" };

bool areEqual = obj1.Equals(obj2);  // true

Обратите внимание, что в приведенном выше примере статический метод Equals не участвует в сравнении на равенство.

Это не отвечает на вопрос.

Enigmativity 21.08.2023 07:51
Ответ принят как подходящий

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

Посмотрите на этот код:

string text = "ASDF123";
NonEmptyString nonEmptyString = NonEmptyString.CreateUnsafe(text);
/* 3 */ Assert.True(text.Equals(nonEmptyString));
/* 5 */ Assert.True(Equals(text, nonEmptyString));

В строке 3 вызов Equals находится в экземпляре string, который понятия не имеет о вашем классе NonEmptyString, поэтому он всегда будет возвращать false независимо от того, равно ли базовое значение NonEmptyString.

В строке 5 вызов Equals находится в экземпляре object, который, опять же, не имеет представления о вашем классе NonEmptyString, поэтому он всегда будет возвращать false независимо от того, равно ли базовое значение NonEmptyString.

Вот оптимизированная компилятором версия вашего кода:

NonEmptyString nonEmptyString = NonEmptyString.CreateUnsafe("ASDF123");
Assert.True("ASDF123".Equals(nonEmptyString));
Assert.True(object.Equals("ASDF123", nonEmptyString));

Вы не контролируете эти Equals перегрузки.


Чтобы сделать вашу жизнь как можно проще, вы должны реализовать == и операторы явного и неявного приведения следующим образом:

public static bool operator ==(string obj1, NonEmptyString obj2) => obj2.Equals(obj1);
public static bool operator !=(string obj1, NonEmptyString obj2) => !obj2.Equals(obj1);
public static bool operator ==(NonEmptyString obj1, string obj2) => obj1.Equals(obj2);
public static bool operator !=(NonEmptyString obj1, string obj2) => !obj1.Equals(obj2);

public static implicit operator string(NonEmptyString nes) => nes.Value;
public static explicit operator NonEmptyString(string text) => NonEmptyString.CreateUnsafe(text);

Проблема в том, что на самом деле работает тот, что в строке 3. text.Equals(NonEmptyString) возвращает true. Вот почему я очень смущен, почему тот, что в строке 5, не возвращает true. Потому что это должен быть просто прокси-метод.

Pavel Kalandra 21.08.2023 08:26

@PavelKalandra - Строка 3 у меня не работала, пока я не реализовал оператор неявного преобразования. Вы определились?

Enigmativity 21.08.2023 08:51

@PavelKalandra - ни один из них не является прокси-методом. string.Equals просто сравнивает string с SpanHelpers.SequenceEqual(ref Unsafe.As<char, byte>(ref strA.GetRawStringData()), ref Unsafe.As<char, byte>(ref strB.GetRawStringData()), (UIntPtr)(uint)(strA.Length * 2)), а object.Equals в конечном итоге использует ==, который сравнивает только ссылки для объектов. Попробуйте string x = "a"; bool result = (((object)"ab") == ((object)(x + "b")));, так как это возвращается False.

Enigmativity 21.08.2023 09:01

О, оснастка, да, у меня есть неявное преобразование. Таким образом, он просто вызывает Equality для строк, а не для объектов. Спасибо. Я принимаю этот ответ. Хотя я должен сказать, что с точки зрения пользователя я ожидаю, что IEquatable изменит поведение статического метода Equals, и хотя теперь я понимаю, почему он не работает, он все еще кажется мне ошибкой. Если бы такое поведение произошло в стороннем nuget, я бы обязательно пошел и попытался это исправить.

Pavel Kalandra 21.08.2023 09:09

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