Комментарии 12
Меня по сих пор удивляет, почему enum
в C# не реализовывает IEquatable<T>
.
EqualityComparer
в данном случае является лишь полумерой, т.к. стоимость одного сравнения остаётся довольно высокой — вызов виртуальной функции.
Microsoft ещё с 2008 года упорно отвечает на предложения по добавлению IEquatable и IComparable к enum в стиле «спасибо, хорошая идея, но у нас нет планов по её добавлению в ближайший релиз C#».
Мне кажется, что при переходе на .NET 2.0 про enum-ы просто забыли, а теперь не хотят добавлять этот функционал из-за обратной совместимости.
Вообще-то, для переводов есть отдельный тип статьи. А у вас даже нет указания на конкретный первоисточник.
Некоторая работа по оптимизации кишочков идет. Например, новый 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 поможет избавиться от виртуального вызова и при этом не будет упаковки (котооорая дороже виртуального вызова).
В случае вызова метода .Equals() у объекта JIT-компилятор может решить на месте, нужно ли здесь делать виртуальный вызов или можно применить оптимизацию. Например, когда класс является sealed, и в нём Equals/GetHashCode переопределены, то вызов будет прямым.
- Но если метод переопределён в потомке, то упаковки не будет.
Теперь вопрос: как реализация IEquatable поможет избавиться от виртуального вызова и при этом не будет упаковки (котооорая дороже виртуального вызова).
Не IEquatable, а IEquatable<T>. Позволит избежать упаковки, т.к. аргументом является не object, а сама структура T. А вызов будет невиртуальным — см. п. 1.
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)
самым быстрым будет третий, второй — где-то в пару раз медленее третьего (для мелких структур), ну а первый — на порядок медленнее.
Перевод на русский английской статьи русскоязычного хабраюзера, который присутствует в комментариях к этому переводу.
To box or not to Box? That is the question