Почему компилятор С# не перехватывает InvalidCastException

Возможный дубликат:
Время компиляции и время выполнения кастинг С#

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

Пример:


public class Post { }
public class Question : Post { }
public class Answer : Post 
{
    public void Fail()
    {
        Post p = new Post();
        Question q = (Question)p; // This will throw an InvalidCastException
    }
}

Мои вопросы...

  1. Если мои предположения неверны, то может ли кто-нибудь привести пример, демонстрирующий, как они неверны?
  2. Если мои предположения верны, то почему компилятор не предупреждает об этой ошибке?

person Jeremy Wiggins    schedule 17.11.2011    source источник
comment
Почему вы ожидаете, что компилятор будет следовать всем возможным путям кода, чтобы определить, что p не был изменен до приведения?   -  person John Saunders    schedule 17.11.2011
comment
И событие, если оно не изменилось, Post может реализовать неявный оператор для приведения себя к Question или наоборот.   -  person PVitt    schedule 17.11.2011
comment
броски для ковбоев, садись на быка и катайся, детка   -  person kenny    schedule 17.11.2011
comment
Как указывали другие, статический анализ для определения того, что это не удастся, нетривиален. Некоторые из более новых инструментов статического анализа найдут подобные вещи, но они достаточно сложны, чтобы не выпадать естественным образом из-за ошибки во время компиляции.   -  person Dan Bryant    schedule 17.11.2011


Ответы (8)


Есть несколько причин, по которым это преобразование разрешено.

Во-первых, как говорили люди в других ответах, оператор приведения означает, что я знаю больше, чем вы; Я гарантирую вам, что это преобразование пройдет успешно, и если я ошибаюсь, выкину исключение и завершу процесс. Если вы лжёте компилятору, произойдут плохие вещи; на самом деле вы не даете эту гарантию, и в результате программа выходит из строя.

Теперь, если компилятор может сказать, что вы ему лжете, он может уличить вас во лжи. Компилятору не требуется быть сколь угодно умным, чтобы поймать вас на вашей лжи! Анализ потока, необходимый для определения того, что выражение типа Base никогда не будет иметь тип Derived, сложен; значительно сложнее, чем логика, которую мы уже реализуем для обнаружения таких вещей, как неназначенные локальные переменные. У нас есть лучшие способы потратить наше время и усилия, чем улучшать способность компилятора уличать вас в очевидной лжи.

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

Во-вторых, можно указать (Derived)(new Base()), где Derived – это тип, который реализует тип Base и который не дает сбой во время выполнения. Также возможен сбой (Base)(new Base()) с недопустимым исключением приведения во время выполнения! Реальные факты! Это чрезвычайно редкие ситуации, но они возможны.

Подробнее читайте в моих статьях на эту тему:

person Eric Lippert    schedule 17.11.2011

В некоторых случаях Post может быть преобразовано в Question. Выполняя приведение, вы говорите компилятору: «Это сработает, я обещаю. Если нет, вы можете сгенерировать недопустимое исключение приведения».

Например, этот код будет работать нормально:

    Post p = new Question();
    Question q = (Question)p;

Приведение прямо заявляет, что вы лучше, чем компилятор, знаете, что это такое на самом деле. Возможно, вы захотите сделать что-то вроде ключевых слов as или is?

person McKay    schedule 17.11.2011
comment
Добавление бананов к моему ответу заставило меня потерять драгоценные моменты, и вы были быстрее. +1 от меня. - person Paolo Tedesco; 17.11.2011
comment
@PaoloTedesco Мне нравится твой пример с бананом. - person McKay; 17.11.2011

Дело в том, что p может быть Question, поскольку вопрос наследуется от Post.
Рассмотрим следующее:

public class Post { }
public class Question : Post { }
public class Banana { }

static class Program {
    public static void Main(params string[] args) {
        Post p = new Question();
        Question q = (Question)p; // p IS a Question in this case
        Banana b = (Banana)p; // this does not compile
    }
}
person Paolo Tedesco    schedule 17.11.2011

Когда вы выполняете явное приведение типов, вы говорите компилятору: «Я знаю то, чего не знаешь ты».

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

person Oded    schedule 17.11.2011

1) Ваше предположение неверно. Кто-нибудь всегда может реализовать явный оператор преобразования для Question для преобразования из Post:

public class Question`
{
    // some class implementation

    public static explicit operator Question(Post p)
    {
        return new Question { Text = p.PostText };
    }
}

2) Явное приведение — это ваш способ сообщить компилятору, что вы знаете лучше, чем он. Если вы хотите что-то использовать, когда вы не уверены, будет ли приведение успешным или нет, и не хотите исключения во время выполнения, используйте операторы is и as.

person Justin Niessner    schedule 17.11.2011
comment
Разве вы не получите, что пользовательские преобразования в базовый класс или из него не разрешены? - person Black Light; 17.11.2011
comment
Автор вопроса уже указал содержимое классов (пусто), поэтому этот ответ недействителен. Он даже не показал их частично или что-то в этом роде, так что, строго говоря, это неприменимо. - person Meligy; 17.11.2011
comment
@MohamedMeligy - ОП, возможно, показал свои реализации ... но компилятору все равно. По-прежнему существует вероятность того, что явная операция преобразования действительно существует, и компилятор не собирается ее проверять. - person Justin Niessner; 17.11.2011

Компилятор рассматривает p как переменную, поэтому не пытается отслеживать ее значение. Если бы это было так, анализ всего приложения занял бы очень много времени. Некоторые инструменты статического анализа похожи на FxCop.

Компилятор видит Post, но не отслеживает присваивание и знает, что возможно:

Post p = new Question();

Так что проходит нормально.

Вы знаете, что не можете сделать:

Question q = p;

Разница в том, что здесь вы пытаетесь сказать компилятору использовать то, что он знает, для проверки этого, и он знает, что Post не обязательно является Question.

В исходной версии вы говорите компилятору: «Я знаю, что это так, и я установлю это явно, уйду с дороги, и я возьму исключение, если то, что я знаю, неверно», поэтому он слушает вас и выходит твой путь!

person Meligy    schedule 17.11.2011

Ваши предположения верны: он скомпилируется и завершится ошибкой во время выполнения.

В вашем небольшом примере очевидно, что приведение потерпит неудачу, но компилятор не знает об этом. Поскольку Post является супертипом Question, вы можете присвоить Question p, и, поскольку вы делаете приведение, вы объявляете о готовности взять на себя некоторую ответственность от компилятора. Если бы вы пытались назначить string или что-то еще, не являющееся частью той же ветки наследования, компилятор должен вас предупредить. И наоборот, вы всегда можете попытаться привести object к любому типу.

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

person faester    schedule 17.11.2011

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

public static class ObjectHelper
{
    public static T Cast<T>(this Object source)
    {
        var destination = (T)Activator.CreateInstance(typeof(T));

        var sourcetype = source.GetType();
        var destinationtype = destination.GetType();

        var sourceProperties = sourcetype.GetProperties();
        var destionationProperties = destinationtype.GetProperties();

        var commonproperties = from sp in sourceProperties
                               join dp in destionationProperties on new { sp.Name, sp.PropertyType } equals
                                   new { dp.Name, dp.PropertyType }
                               select new { sp, dp };

        foreach (var match in commonproperties)
        {
            match.dp.SetValue(destination, match.sp.GetValue(source, null), null);
        }

        return destination;
    }
}

К вашему сведению, это, вероятно, будет работать, только если два объекта существуют в одной сборке.

Большая часть кода взята отсюда: Mapping Business Objects and Объект Entity с отражением c#

person bygrace    schedule 17.11.2011