Почему операции приведения и преобразования синтаксически неразличимы?

У Stack Overflow есть несколько вопросов о приведении значений в штучной упаковке: 1, 2.

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

Более того, та же проблема актуальна для ссылочных типов:

void Main()
{
    object obj = new A();
    B b = (B)obj;   
}


public class A 
{
}

public class B {}

Этот код выдает InvalidCastException. Так что дело не в значении и типе ссылки; так ведет себя компилятор.

Для верхнего кода выдает castclass B, а для кода

void Main()
{
    A obj = new A();
    B b = (B)obj;   
}

public class A 
{
    public static explicit operator B(A obj)
    {
        return new B();
    }
}

public class B
{
}

он излучает call A.op_Explicit.

Ага! Здесь компилятор видит, что A имеет оператор и вызывает его. Но что тогда произойдет, если B наследуется от A? Не так быстро, компилятор довольно умный... он просто говорит:

A.явный оператор B(A)': определяемые пользователем преобразования в производный класс или из него не допускаются.

Ха! Никакой двусмысленности!

но с какой стати они допустили, чтобы две довольно разные операции выглядели одинаково?! В чем причина?


person Pavel Voronin    schedule 13.03.2015    source источник
comment
Вы также можете создавать неявные преобразования, если хотите. Тогда это будет B b = obj;.   -  person juharr    schedule 13.03.2015
comment
Во-первых, это плохой вопрос для Stack Overflow, потому что на самом деле на него может определенно ответить только кто-то из команды разработчиков языка C# 1.0. Я не верю, что кто-то из этих людей в настоящее время отвечает здесь на множество вопросов. Я был бы рад попробовать это, но я не могу понять, какой вопрос вы на самом деле задаете здесь.   -  person Eric Lippert    schedule 13.03.2015
comment
@EricLippert Почему операторы приведения и преобразования выглядят одинаково?   -  person Pavel Voronin    schedule 13.03.2015
comment
@voroninp Это именно то, что имел в виду Эрик - это философский вопрос, который предлагает людям сделать предположение, но вы не получите определенного ответа, если только дизайнеры языка не окажутся рядом.   -  person Wormbo    schedule 13.03.2015
comment
@Эрик Липперт. Кстати, ECMA-335 не запрещает реализацию преобразования в операторы базового класса (т.е. специальные методы) в производном классе. И похоже, что это было запрещено в C# только для того, чтобы избежать двусмысленности между операциями приведения и преобразования.   -  person Pavel Voronin    schedule 13.03.2015
comment
@Wormbo Когда вы говорите о догадках, я согласен лишь частично. Поскольку форма фюзеляжа самолета определяется конструкцией и законами природы, то же самое верно (по крайней мере, я так считаю) для языков программирования. Мы не можем сказать за . Таким образом, кроме скрытых намерений, для такого решения должна быть какая-то общая рациональность. И догадка иногда может быть очень проницательной.   -  person Pavel Voronin    schedule 13.03.2015
comment
Ну, если вы так выразились, преобразование между, например. int и float всегда использовали тот же синтаксис, что и приведение между ссылками на объекты в языках C-стиля. Нет причин использовать другой синтаксис для явных преобразований типов в C#, которые по существу аналогичны преобразованию значения int в число с плавающей запятой.   -  person Wormbo    schedule 13.03.2015


Ответы (2)


Насколько я могу судить, ваше наблюдение — это наблюдение, которое я сделал здесь:

http://ericlippert.com/2009/03/03/representation-and-identity/

Есть два основных варианта использования оператора приведения в C#:

(1) В моем коде есть выражение типа B, но у меня больше информации, чем у компилятора. Я утверждаю, что точно знаю, что во время выполнения этот объект типа B на самом деле всегда будет производным типом D. Я сообщу компилятору об этом утверждении, вставив в выражение приведение к D. Поскольку компилятор, вероятно, не может проверить мое утверждение, компилятор может убедиться в его правдивости, вставив проверку во время выполнения в том месте, где я делаю утверждение. Если мое утверждение окажется неточным, CLR выдаст исключение.

(2) У меня есть выражение некоторого типа T, которое, как я точно знаю, не относится к типу U. Однако у меня есть хорошо известный способ связать некоторые или все значения T с «эквивалентным» значением U. Я буду указать компилятору сгенерировать код, который реализует эту операцию, вставив приведение к U. (И если во время выполнения окажется, что не существует эквивалентного значения U для конкретного T, которое у меня есть, мы снова выбрасываем исключение.)

Внимательный читатель заметит, что это противоположности. Ловкий трюк, чтобы иметь оператор, который означает две противоречивые вещи, не так ли?

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

Ваш вопрос, почему это так? Это не хороший вопрос! :-)

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

Когда команда разработчиков языка C# разрабатывает функции, дебаты могут продолжаться буквально месяцами, они могут включать дюжину человек, обсуждающих множество различных предложений, каждое со своими плюсами и минусами, что приводит к сотням страниц заметок. Даже если бы у меня была соответствующая информация со встреч конца 1990-х годов об операциях с актерами, которой у меня нет, мне кажется трудным кратко обобщить ее таким образом, который удовлетворил бы первоначального спрашивающего.

Более того, чтобы удовлетворительно ответить на этот вопрос, конечно, пришлось бы обсудить всю историческую перспективу. C# был разработан, чтобы быть сразу же продуктивным для существующих программистов C, C++ и Java, и поэтому он заимствует многие из соглашений этих языков, включая его основные механизмы для операторов преобразования. Чтобы правильно ответить на вопрос, нам также пришлось бы обсудить историю оператора приведения типов в C, C++ и Java. Кажется, слишком много информации, чтобы ожидать ответа на StackOverflow.

Откровенно говоря, наиболее вероятным объяснением является то, что это решение не стало результатом долгих споров о достоинствах различных позиций. Скорее всего, команда языковых дизайнеров обдумала, как это делается в C, C++ и Java, приняла разумную компромиссную позицию, которая не выглядела слишком уж ужасной, и перешла к другим, более интересным делам. Таким образом, правильный ответ был бы почти полностью историческим; почему Ричи разработал оператор приведения так же, как он сделал для C? Я не знаю, и мы не можем спросить его.

Мой вам совет: перестаньте спрашивать, почему? вопросы об истории дизайна языка программирования и вместо этого задавайте конкретные технические вопросы о конкретном коде, вопросы, на которые есть краткие ответы.

person Eric Lippert    schedule 13.03.2015
comment
Интересно отметить, что C++ отказался от приведения типов в стиле C в качестве рекомендации и добавил несколько операторов преобразования, чтобы более точно указать, какой тип приведения выполняется. - person Guvante; 13.03.2015
comment
@Guvante: отличный момент. Затем можно было бы задать дополнительный вопрос (плохой для SO). Если это такая хорошая идея, то почему С# не последовал примеру С++ здесь? а теперь у нас есть почему бы и нет? вопрос о причинах не реализовывать сложную, запутанную и дорогостоящую функцию. Вот почему я воздерживаюсь от ответов на вопросы «почему?» и «почему бы и нет». - person Eric Lippert; 13.03.2015
comment
Ну, первоначальное намерение состояло в том, чтобы спросить, как преобразовать из упакованного значения в определенный тип. Когда, например, DataReader[0] содержит byte, мы можем преобразовать его в int только с помощью (int)(byte)DataReader[0]. А потом я узнал, что требование универсально для всех типов и посмотрел на сгенерированный IL. Разработчики C# допускают небезопасные приведения типов, но не допускают небезопасных преобразований. В моей ситуации у меня есть третий случай для ваших первых двух из статьи: у меня есть значение некоторого типа, и я ожидаю, что у него будет оператор преобразования в нужный мне тип. Мне все равно, какой это тип на самом деле. - person Pavel Voronin; 14.03.2015
comment
@voroninp: генерация кода для преобразования из упакованных значений в другие типы требует, по сути, запуска правил преобразования компилятора во время выполнения; если это то, что вы хотите сделать, используйте dynamic или любой из нескольких вспомогательных методов, которые делают именно это, и получите удар по производительности. - person Eric Lippert; 14.03.2015
comment
@voroninp: в своей исходной статье я отмечаю, что для приведения может быть более двух возможных значений; эти два были как раз теми, которые касались меня для целей этой статьи. - person Eric Lippert; 14.03.2015
comment
И компилятор разумно показывает ошибку, когда я пытаюсь использовать "(TGenericTypeParam)someVariable". А если бы я только мог ;-) Ниже в своей статье вы пишете, что способов конвертации может быть много, а здесь я говорю только о стандартном операторе. - person Pavel Voronin; 14.03.2015
comment
@EricLippert Похоже, мне нужно углубить свои знания о CLR. Я (даже не спрашивайте, почему) думал, что можно сказать, что это просто вызывает статический метод типа этого экземпляра с именем op_Explicit, возвращающим определенный тип. Как я теперь вижу, это было ошибочное предположение. Своего рода полиморфное поведение для статических методов. - person Pavel Voronin; 14.03.2015
comment
@voroninp: Действительно, это довольно часто запрашиваемая функция. - person Eric Lippert; 14.03.2015

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

Ваш первый пример кода по сути выглядит как «преобразование из объекта в B», поскольку компилятор понятия не имеет, что переменная может содержать только A. Согласно правилам, это означает, что компилятор должен выполнить операцию приведения типов.

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

person Wormbo    schedule 13.03.2015
comment
Это не отвечает на вопрос, почему операции приведения и преобразования синтаксически неразличимы? - person juharr; 13.03.2015
comment
@juharr Однако этот вопрос не подходит для StackOverflow, поскольку никто, кроме разработчиков этого языка, не может предложить ничего, кроме предположений. - person Dan J; 13.03.2015