Как стать автором
Обновить

Комментарии 12

Меня по сих пор удивляет, почему enum в C# не реализовывает IEquatable<T>.
EqualityComparer в данном случае является лишь полумерой, т.к. стоимость одного сравнения остаётся довольно высокой — вызов виртуальной функции.

Я подозреваю, что сложность в предложенном подходе заключается в том, что JIT-у пришлось бы много работать, чтобы это сделать. Например, реализация IList массивом является достаточно нетривиальной штукой.
А с какой стати JIT тут будет напрягаться, если enum — это просто int?

Microsoft ещё с 2008 года упорно отвечает на предложения по добавлению IEquatable и IComparable к enum в стиле «спасибо, хорошая идея, но у нас нет планов по её добавлению в ближайший релиз C#».

Мне кажется, что при переходе на .NET 2.0 про enum-ы просто забыли, а теперь не хотят добавлять этот функционал из-за обратной совместимости.

Вообще-то, для переводов есть отдельный тип статьи. А у вас даже нет указания на конкретный первоисточник.

Спасибо за замечания. Указания на первоисточник есть в начале статьи. Тип статьи я вначале не смог найти (ее действительно трудно заметить). Написал в поддержку, показали где выбирается.
Enum — это не просто int. Это кастомный тайп, отнаследованный от System.Enum, который отнаследован от System.ValueType, который содержит примитив внутри. Это может быть int, а может быть short или byte. Именно за счет того, что поля могут быть разного типа mscorlib содержит семейство «сравнителей»: SByteEnumEqualityComparer, и простой EnumEqualityComparer.

Некоторая работа по оптимизации кишочков идет. Например, новый JIT знает о существовании Enum.HasFlags. Плюс идет работа по девиртуализации вызовов.

Да, и я не совсем понял, как реализация IEquatable поможет избавиться от виртуального вызова. Можете пояснить этот момент?
Да, и я не совсем понял, как реализация IEquatable поможет избавиться от виртуального вызова. Можете пояснить этот момент?

Очень просто: вызов .Equals() для value-типов — невиртуальный, тогда как вызов EqualityComparer.Equals() — виртуальный.

Простите, но я не понимаю:).

1. Наличие IEquatable никак не влияет на то, будет ли вызов Equals/GetHashCode виртуальным или нет.
2. Вызов метода, определенного в System.ValueType или System.Enum приводит к упаковке:
int n = 42; var t = n.GetType(); — невиртуальный, но приводит к упаковке.

Теперь вопрос: как реализация IEquatable поможет избавиться от виртуального вызова и при этом не будет упаковки (котооорая дороже виртуального вызова).
  1. В случае вызова метода .Equals() у объекта JIT-компилятор может решить на месте, нужно ли здесь делать виртуальный вызов или можно применить оптимизацию. Например, когда класс является sealed, и в нём Equals/GetHashCode переопределены, то вызов будет прямым.


  2. Но если метод переопределён в потомке, то упаковки не будет.

Теперь вопрос: как реализация IEquatable поможет избавиться от виртуального вызова и при этом не будет упаковки (котооорая дороже виртуального вызова).

Не IEquatable, а IEquatable<T>. Позволит избежать упаковки, т.к. аргументом является не object, а сама структура T. А вызов будет невиртуальным — см. п. 1.

1. Эта возможность есть и сейчас и для этого IEquatable не нужен.
2. Потомка не существует. Для этого нужно, чтобы каждый enum был унаследован от System.Enum.

Это все я к тому, что для того, чтобы избежать виртуального вызова и упаковке совсем не обязательно, чтобы перечисления реализовывали интерфейс. Для этого необходимо и достаточно, чтобы JIT-компилятор знал о них, аналогично тому, как он сейчас уже знает о Enum.HasFlags.

Напомню, что речь идёт о сравнении generic типов, а не о сравнениях вообще.


Это все я к тому, что для того, чтобы избежать виртуального вызова и упаковке совсем не обязательно, чтобы перечисления реализовывали интерфейс

Нет. Реализация интерфейса обязательна, если мы хотим делать эффективные сравнения в generic методах. То есть требовать от типа, чтобы он был IEquatable<T>. Но если для примитивных типов этот интерфейс определён, то для перечислений его, упс, нету.


Из следующих трёх вариантов:


bool Func1<T>(T v1, T v2)  => v1.Equals(v2);   // Будет вызван object.Equals(object other), со всеми его минусами

bool Func2<T>(T v1, T v2) => EqualityComparer<T>.Default.Equals(v1, v2);    // Будет вызван эффективный компаратор, но ценой одного виртуального вызова, т.к. EqualityComparer<T>.Default возвращает абстрактный класс с виртуальным Equals

bool Func3<T>(T v1, T v2) where T : IEquatable<T> => v1.Equals(v2);    // Будет вызван T.Equals(T other)

самым быстрым будет третий, второй — где-то в пару раз медленее третьего (для мелких структур), ну а первый — на порядок медленнее.

До чего дошел прогресс…
Перевод на русский английской статьи русскоязычного хабраюзера, который присутствует в комментариях к этому переводу.
image
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории