Нюансы кастования в C#

в 20:03, , рубрики: C#, интерфейсы, кастование

Всем привет. Не так давно добавлял поддержку кастования через '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);

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

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

Автор: crackanddie

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js