Страницы

Поиск по вопросам

пятница, 5 октября 2018 г.

Методы с одинаковыми сигнатурами в C#

Предположим, есть такой класс.
public class SomeClass { public void DoSomething(int x) { Console.WriteLine("DoSomething(int x) called"); }
public void DoSomething(T x) { Console.WriteLine("DoSomething(T x) called"); } }
А также вот такой фрагмент кода
var someObj = new SomeClass();
У меня два вопроса.
1) У закрытого типа SomeClass получаются два метода с одинаковой сигнатурой. Тогда почему компилятор же не ругается на них?
2) Скажем, если вызвать
someObj.DoSomething(0);
Данный фрагмент кода пишет "DoSomething(int x) called". Почему был вызван именно вариант с целочисленным параметром, а не с обобщённым параметром?


Ответ

Выдержка из спецификации (пункт 7.5.3.2):
... In case the parameter type sequences {P1, P2, …, PN} and {Q1, Q2, …, QN} are equivalent (i.e. each Pi has an identity conversion to the corresponding Qi), the following tie-breaking rules are applied, in order, to determine the better function member. • If MP is a non-generic method and MQ is a generic method, then MP is better than MQ. ...
по русски это звучит примерно так:
Если последовательности параметров эквивалентны, применяются следующие правила выбора лучшего метода: Если метод Mp не обобщенный и метод Mq обобщенный, то Mp лучше чем Mq
Там еще много правил отсева, но для этого случая остальные правила несущественны.
Ок, откуда ноги растут выяснили, данное поведение прописано в спецификации. Как это выглядит в реализации? Давайте заглянем в скомпилированный код и сравним обобщенный и не обобщенный методы. Я использую ILSpy, но можно воспользоваться любым знакомым инструментом, например ILDasm.
// Methods .method public hidebysig instance void DoSomething ( int32 x ) cil managed { // Method begins at RVA 0x205a // Code size 11 (0xb) .maxstack 8
IL_0000: ldstr "DoSomething(int x) called" IL_0005: call void [mscorlib]System.Console::WriteLine(string) IL_000a: ret } // end of method SomeClass`1::DoSomething
.method public hidebysig instance void DoSomething ( !T x ) cil managed { // Method begins at RVA 0x2066 // Code size 11 (0xb) .maxstack 8
IL_0000: ldstr "DoSomething(T x) called" IL_0005: call void [mscorlib]System.Console::WriteLine(string) IL_000a: ret } // end of method SomeClass`1::DoSomething
как видите, код обобщенного метода класса остался обобщенным после компиляции (обратите внимание на ! перед именем типа во втором методе).
На вопрос почему выбор происходит так, а не наоборот ответили. Теперь немного о возможных причинах такой реализации и правил выбора. Возможных, потому что я не являюсь разработчиком данной спецификации, но из общей логики некоторые выводы сделать можно.
В обобщенных методах вы ограничены в применении языковых средств к переменным обобщенного типа. Например нельзя применять операторы сравнения, кроме проверок на равенство и не равенство, т.к. мы не можем гарантировать что они реализованы в типе, для которого будет вызван обобщенный метод. Соответственно при наличии подходящего не обобщенного метода, с большой вероятностью он может быть реализован более эффективно за счет использования специализированных средств конкретного типа. Перед вызовом обобщенного метода, компилятор должен заменить обобщенные типы на реальные. Эти действия не требуются при использовании не обобщенных методов, а значит даже в тривиальных случаях, когда эффективность методов одинакова, стоимость вызова обобщенного метода незначительно, но выше.
Получается что обобщенные методы немного теряют производительность, а что в замен? Ведь у нас есть общий корень - тип object, к которому также можно привести любой другой тип. Я вижу это так:
При использовании обобщенных методов не используется приведение типов как в случае использование object, а это выигрыш в скорости, особенно для ValueType типов, т.к. отсутствует боксинг. При использовании обобщенных типов можно быть уверенным, что если у двух переменных указан один и тот же обобщенный тип. то при выполнении их типы также будут одинаковы, в отличие от переменных типа object, внутри которых может лежать что угодно, и требуются дополнительные проверки для исключения ошибок во время выполнения. При использовании обобщенных типов, мы можем использовать уточнения или ограничения, кому как нравится, и явно указать: класс это или структура, наличие конструктора (пока только конструктор по умолчанию), базовый тип, а также список интерфейсов, которые должны быть реализованы в типах, которые разрешено использовать в конкретном обобщенном методе или классе. Этот вариант, как и предыдущий, для типа object можно реализовать только путем дополнительных проверок и увеличения объема необходимого кода.
Естественно я могу ошибаться насчет истинных причин конкретно такой реализации. Естественно списки преимуществ и недостатков далеко не полные. Но я считаю что этого достаточно для общего понимания почему обобщенные типы и все с ними связанное такие, какие есть и применения их по назначению и с пользой для дела.

PS: Все, кроме цитаты из спецификации и IL-кода, является моим личным мнением, если вы с ним не согласны, то мы можем обсудить это в чате или вы можете написать собственный ответ на данный вопрос и изложить свою точку зрения.
Отдельное спасибо @Vadim Ovchinnikov за критику предыдущего варианта.

Комментариев нет:

Отправить комментарий