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





Я согласен с первым определенно - со вторым в большинстве случаев («никогда не говори никогда»), но не буду волноваться от третьего; помимо всего прочего, он создает ненужное различие между структурами и классами. В некоторых случаях наличие неявного преобразования может значительно упростить соглашение о вызовах.
Для явных преобразований возможно все.
Однако не так часто нужно писать операторы преобразования. И во многих случаях явные операторы более полезны с учетом того, что они могут нарушиться, если условия не верны (например, с Nullable<T>, если у них нет значения).
Первый не так прост, как можно было ожидать. Вот пример:
using System;
class Test
{
static void Main()
{
long firstLong = long.MaxValue - 2;
long secondLong = firstLong - 1;
double firstDouble = firstLong;
double secondDouble = secondLong;
// Prints False as expected
Console.WriteLine(firstLong == secondLong);
// Prints True!
Console.WriteLine(firstDouble == secondDouble);
}
}
Лично я очень редко создаю свои собственные неявные преобразования. Я достаточно доволен теми, что включены в структуру, но я редко думаю, что добавление моих собственных улучшит жизнь. (То же самое верно и для типов значений в целом, кстати.)
Обновлено: просто чтобы немного ответить на вопрос, вероятно, стоит прочитать операторы преобразования - часть рекомендаций по проектированию библиотеки классов Microsoft.
Вы действительно погружаетесь в странности, не так ли ?? Откуда вы взяли это? :)
@Andrew: ну, у них обоих 64 бита, и не каждое двойное число является целым числом. Разумеется, будут некоторые целые числа, которые невозможно представить в точности.
Не считаю удивительным тот факт, что firstDouble==secondDouble. Что я считаю "удивительным", так это то, что firstLong == secondDouble компилируется без диагностики и возвращает истину. Нельзя ожидать, что назначения firstDouble и secondDouble сделают что-либо, кроме как заставить firstDouble содержать лучшие доступные double представления firstLong и secondLong. Для сравнения, поскольку == имеет перегрузку, первый тип оператора которой требует преобразования, а другой - нет, я бы предпочел диагностику компилятора.
@supercat: в обоих случаях преобразование неявное, но может потерять информацию. Я бы хотел, чтобы оба преобразования вызывали предупреждение / ошибку, или ни то, ни другое.
По идее, нет причин, по которым == должен преобразовывать свои аргументы в один и тот же тип; некоторые перегрузки смешанного типа могут иметь значения, недоступные другими способами [например, (лонг, улонг) и (улонг, лонг)]. Я бы сказал, что, когда тип назначения очевиден, «нечеткое» преобразование без приведения типов яснее, чем преобразование с использованием, поскольку большинство явных приведений типов гарантирует, что в случае успеха исходные значения будут точно представлены в типе назначения. Если сказать, что double X = someLong;, то будет ясно, что X будет значением, подходящим для double, независимо от того, подходит ли someLong.
99% всего, что делают компьютеры, «теряет информацию». Удаление информации который никогда не понадобится - это хорошо. Как вы думаете, g.DrawLine(myPen, (float)myX1, (float)myY1, (float)myX2, (float)myY2) чище, чем g.DrawLine(myPen, myX1, myY1, myX2, myY2)? Дополнительная точность double может иметь отношение к окружающему коду, но не имеет отношения к DrawLine. ИМХО, foo=something; должен компилироваться без диагностики, если это, вероятно, заставит foo содержать лучшее или, по существу, одинаково хорошее представление всего, для чего something имеет лучшее представление.
@supercat: Опять же, я думаю, что мы не собираемся соглашаться, и темы комментариев SO не являются подходящей средой для обсуждения. Если мы когда-нибудь встретимся лично, будет о чем поговорить ...
Я бы сказал, что для любой пары значений типов T и U, если var myT = (T)someU; return (myT == otherU) не должен возвращать ничего, кроме (someU == otherU). Если он не может этого обещать, он должен генерировать исключение при явном приведении типов или вообще не компилировать. Однако, если T - это float, а U - это double, это не сработает. При преобразовании в float, явном или неявном, говорится: «Удалите младшие биты, поскольку они мне не нужны». Однако использование float в качестве double предполагает, что биты, вероятно, все-таки нужны - их просто не существует (упс).
@supercat: Пожалуйста, посмотрите мой предыдущий комментарий. Я действительно нет собираюсь втянуться в это.
Справедливо. Я предполагаю, что моя первоначальная точка зрения заключалась в том, что что касается вашего примера [причина для комментария здесь], даже люди, которые понимают, как работает double, и не удивлены firstDouble == secondDouble, могут все еще находить firstDouble == secondLong удивительным (поскольку не очевидно, что он должен быть эквивалентен firstDouble == (double)secondLong, а не (ExtendedDouble)firstDouble == (ExtendedDouble)secondLong).
Пограничный случай, который заставил меня задать этот вопрос, является нарушением второго. А именно, у меня есть класс Lazy <T> (разве не все?), И я начал размышлять, не следует ли мне выполнять неявное преобразование в T. Мой инстинкт - сказать да, и текущая версия делает это, но я не слишком Конечно.
Нет, я действительно не стал бы - поскольку вы потенциально делаете значительную работу, кажется неправильным делать это неявным. Возможно, у вас есть преобразование явный - но если вы ищете, "почему это внезапно выполняет какой-то код?" неявное преобразование слишком легко пропустить.
Поразмыслив еще немного, я все-таки склонен согласиться.
Или даже такой метод, как GetValue ()
Await () в моем случае, унаследованный от Future <T>
Если вы будете поддерживать неявные преобразования между объектами ваших собственных типов, я бы посоветовал вам решить, какие «аксиомы» следует применять к таким преобразованиям (например, может быть полезно решить, что если все a==b, b==c и a==c являются допустимо, и два из них верны, третий также должен быть верным), а затем определите наиболее полезный набор преобразований, которые позволят сохранить эти аксиомы. Я обсуждаю четыре полезные аксиомы в своем блоге на http://supercatnet.blogspot.com/2013/09/axioms-of-implicit-type-conversion.html (к сожалению, .NET Framework включает преобразования, которые нарушают все четыре, но запрещает преобразования, которые не должны были бы).
Важно учитывать, что неявные преобразования неточных типов к в большинстве контекстов представляют меньший риск удивления, чем преобразования неточных типов из, но есть заметное исключение: если в метод или оператор передается что-то более точного типа. который имеет перегрузки, которые принимают как более точный тип, так и менее точный тип, в который он может быть преобразован, некоторый уровень «удивительного» поведения будет почти неизбежен, если не сделать неявное преобразование невозможным [например, поведение, если программист записывает if (someLong == someFloat), может быть удивительным, но не потому, что потеря точности при неявном преобразовании long в float удивительна, а потому, что существует как минимум шесть разных способов сравнить long и float (*) , и любой, означающий, что компилятор может подключиться к прямому сравнению, удивит тех, кто ожидал чего-то другого. Единственное известное мне решение, позволяющее избежать такого удивления, - это предоставить перегрузки для явного охвата всех неоднозначных случаев и пометить их тегом [Obsolete()]. Это может быть несколько неудобно, но даст существенное преимущество, заключающееся в возможности удовлетворить все четыре аксиомы.
(*) Программист, вероятно, намеревается проверить, равняется ли long значению числа с плавающей запятой, округленному до ближайшего, усеченному до нуля или уменьшенному до отрицательной бесконечности; В качестве альтернативы, программист может захотеть проверить, является ли float тем, который представляет long, имеют ли float и long одинаковую номинальную стоимость или оба float и long будут конвертироваться в один и тот же double.
Ой! Что ж, это скорее подсказывает мне, что это неявное преобразование немного не работает.