Всем привет. Не так давно добавлял поддержку кастования через 'as' к себе в компилятор и задался вопросом - в каких случаях я получу Compile Time ошибку? Если заинтересовал - прошу под кат.

Решил начать с простого:

class Dog { }
class Cat { }

Dog dog = new Dog();
Cat cat = dog as Cat; // error: CS0039 Cannot convert type 'Dog' to 'Cat'

Тут, вроде, все логично: при кастовании смотрим - является ли тип кастуемого экземпляра дочерним от типа, к которому кастуем; или является ли тип кастуемого экземпляра родительским от типа, к которому кастуем. Если одно из условий верно, то ошибок во время компиляции возникать не должно.

Немного усложним ситуацию - добавим интерфейсы:

interface IBarkable { }
class Dog : IBarkable { }
interface IMeowable { }
class Cat : IMeowable { }

Dog dog = new Dog();
IMeowable cat = dog as IMeowable;

В этом случае никаких ошибок не возникает. Но почему? Не разобравшись в вопросе, я решил - "Да ладно, просто буду смотреть, если пользователь кастует экземпляр класса к интерфейсу - не будем ругаться". Добавил соответствующие проверки в компилятор и закоммитил.

Но где-то в глубине души я все еще задавался вопросом - "Почему же оно так, не может же быть все так просто?". Ведь, это действительно так странно, почему компилятор C# не выдает мне ошибку на этапе компиляции? Он же видит, что класс Dog никак не реализует интерфейс IMeowable.

Видимо Очевидно, мои проверки не были верны, так как следующий код:

Dog dog = new Dog();
IMeowable cat = dog as IMeowable;
Dog dogAgain = cat as Dog; // тоже без ошибок компиляции

компилировался тоже без ошибок. И что это значит? Что мы любой класс можем кастовать к любому интерфейсу и наоборот? Но почему? Почему компилятор не предостерегает нас от этого?

Честное слово, я гуглил, гуглил достаточно. Возможно, по всем сайтам, которые я посетил, можно было бы и добыть всю нужную мне информацию. Но я не смог. Не удержался. Через пару минут ChatGpt уже пытался объяснить мне, почему так происходит. Примерный ответ по памяти:

Вооот, там тяжело проверить это все на этапе компиляции и т.д, и т.п.

Сидел и думал - либо я дурак, либо сани не едут я чего-то не понимаю в проверках наследования/имплементации. Ну, как так можно, не суметь проверить имплементации интерфейсов. Да, дольше, чем просто проверять наследование, но реализуемо! Или нет?

Тут меня осенило, я забыл про хитрую "фичу" C# - класс, который напрямую (либо через наследуемые типы) не реализует конкретный интерфейс, все еще может без проблем кастоваться к нему, но с "небольшим условием". И это условие заключается в следующем:

Представим, что мы написали такой прекрасный код и собрали его в библиотеку:

public class Dog { }
public interface IMeowable 
{
    string SayMeow();
}
public class Cat : IMeowable 
{
    public string SayMeow()
    {
        return "Cat says 'Meow'";
    }
}

public class CoolClass
{
    public static string DogMeows(Dog dog)
    {
        IMeowable meowable = dog as IMeowable;
        return meowable.SayMeow();
    }
}

Если бы я увидел такой код до написания этой статьи, я бы подумал - "А в чем суть метода DogMeows, если Dog не реализует интерфейс IMeowable?". Но теперь же прошу - вот ответ на этот вопрос:

Представим, что нашу библиотеку подключил странный конечный пользователь и написал такое:

class DogThatMeows : Dog, IMeowable
{
    public string SayMeow()
    {
        return "Dog says 'Meow'";
    }
}

var strangeDog = new DogThatMeows();
var result = CoolClass.DogMeows(strangeDog);

В этих строчках кода и показан ответ на наш изначальный вопрос. Пользователь нашей библиотеки, коллеги по проекту или даже мы сами никогда не знаем, каким классом будет реализован конкретный интерфейс.

Теперь, полностью разобравшись в проблеме, я спокойно удаляю все проверки на этапе компиляции связанные с кастованиями через интерфейсы. Спасибо за внимание.

Комментарии (15)


  1. AgentFire
    15.02.2025 20:20

    Ох уж эти любители никогда не использовать sealed на классы :)

    Вы же их всегда таким образом проектируете под наследование, хотя почти никогда - специально этого не желаете, и уж точно не готовите к этому начинку класса (нет виртуальных членов), или же начинки других классы, взаимодействующие с этим. Появляется риторический вопрос "зачем".

    Докиньте этот атрибут на Dog, и ошибка должна появиться.


    1. VanKrock
      15.02.2025 20:20

      Ох уж эти любители использовать sealed на классы:)

      Вы же их всегда таким образом проектируете под запрет наследования и уж точно не потому, что объект вашего класса должен иметь строгую схему, для интеграции с нативно компилируемуми языками.

      Нужно понимать, что sealed это именно запрет на наследование, а у запрета должна быть причина... ну разве что вы не одна из наших госструктур. Почему вы на этапе разработки своего кода уверены, что разработчик, который будет использовать вашу библиотеку не должен расширять ваш класс? Почему вы считаете что знаете лучше, что нужно пользователю вашего класса, настолько, что отбираете у него один из принципов ООП?

      P. S. Если бы нужно было запретить наследование для всех классов, что не подготовлены для наследования, то класс был бы sealed по-умолчанию, для того, чтобы его разрешить наследовать нужно было бы использовать какое-нибудь ключевое слово

      P. P. S. Ваш подход на самом деле имеет место, но в функциональном программировании, а не в ООП


      1. xSVPx
        15.02.2025 20:20

        Так он не запрещает наследование. Вы можете им воспользоваться, но придется изменить родительский класс.

        Собственно, в реальной ситуации редко когда можно что-то успешно отнаследовать от чего-то, что является черным ящиком. Т.е. увы, но родительский класс должен это наследование предполагать. Ну или потом грабли весьма вероятны.

        Некоторые вон, разрешают переопределять только абстрактные методы. И в этом определенная логика тоже есть. Хотя это и офигенно неудобно, особенно для изготовления заплаток в каком-нибудь адском легаси.


  1. NN1
    15.02.2025 20:20

    Не ясен посыл статьи. Это для новичков или для более опытных ?

    В общем случае лучше не использовать “as” совсем.

    Во первых, стоит работать с Nullable Reference ( #nullable enable ), тут сразу всплывёт то, что “as” может вернуть null.

    Во вторых, лучше использовать приведение типов если нужен определённый тип, таким образом мы не ошибёмся и не пропустим null.

    Ну и наконец если хочется только проверить и если подходит использовать, то сопоставление с образцом ( is IMewoable m ) подойдёт лучше.


    1. Proscrito
      15.02.2025 20:20

      Да, настоящие бро давно используют конструкцию if(animal is Cat cat), ну и далее по накатанной :)

      Автор еще до unsafe кастов не добрался. А в современном дотнете там под капотом уже бескрайние поля оптимизации через остроумные трюки с памятью, в которых еще несколько лет назад строго типизированный язык побоялся бы открыто признаться. И, надо сказать, эти извращения очень заразительны...


    1. VanKrock
      15.02.2025 20:20

      У меня сложилось впечатление, по мере прочтения статьи, что ожидания автора от as как от явного преобразования типов


      1. V1ruS1989
        15.02.2025 20:20

        Здесь неточность есть?
        Здесь неточность есть?


  1. ColdPhoenix
    15.02.2025 20:20

    а если в метод DogMeows, принимать IMeowable который ему действительно нужен, проблемы так же не будет.


  1. bossalex
    15.02.2025 20:20

    Что такое "КАСТОВАНИЕ"?


    1. xSVPx
      15.02.2025 20:20

      Эш назг турбопаскаль...


    1. ryanl
      15.02.2025 20:20

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


    1. BombaBomba
      15.02.2025 20:20

      Что такое "КАСТОВАНИЕ"?

      Использование заклинаний.


    1. ZODIACality
      15.02.2025 20:20

      ну, если вкраце и для простого понимания, это приведение типов от родительского класса к дочернему или наоборот, в первом случае поля и методы дочернего просто скрываются, но все еще существуют, и их в будущем можно вернуть кастованием типа к дочернему классу, а вообще погугли, тема известная


  1. commanderkid
    15.02.2025 20:20

    Я так понял раньше проблем небыло, так как as возвращал в случае неуспеха null, а теперь будет типа того, что неналбл тип приравнивают к null и получают в лоб exception, что-то типа NullReferenceException, а не какой-нибудь InvalidCastException(если такой есть).


  1. Naf2000
    15.02.2025 20:20

    Встречаются и преобразования между интерфейсами, вот живой пример из LinqToDB:

    		public static Query<T> GetQuery(IDataContext dataContext, ref IQueryExpressions expressions, out bool dependsOnParameters)
    		{
    			...
    					if (dataContext is IExpressionPreprocessor preprocessor)
    						expr = preprocessor.ProcessExpression(expr);