Подтвердить что ты не робот

Добавление int и uint

Меня удивляет поведение компилятора С# в следующем примере:

int i = 1024;
uint x = 2048;
x = x+i;     // A error CS0266: Cannot implicitly convert type 'long' to 'uint' ...

Кажется ОК, поскольку int + uint может переполняться. Однако, если uint изменено на int, тогда ошибка исчезает, например int + int не может выдавать переполнение:

int i = 1024;
int x = 2048;
x = x+i;     // OK, int

Кроме того, uint + uint = uint:

uint i = 1024;
uint x = 2048;
x = x+i;     // OK, uint

Кажется совершенно неясным.

Почему int + int = int и uint + uint = uint, но int + uint = long?

Какова мотивация этого решения?

4b9b3361

Ответ 1

Короткий ответ - "потому что стандарт говорит, что он должен быть таким", который см. в информационном разделе 14.2.5.2 ИСО 23270. Нормативный и сект 13.1.2. (Неявные числовые преобразования) говорит:

Неявные числовые преобразования:

...

  • От int до long, float, double или decimal.
  • От uint до long, ulong, float, double или decimal.

...

Конверсии от int, uint, long или ulong до float и от long или ulong до doubleможет привести к потере точности, но никогда не приведет к потере величины. Другие неявные числовые преобразования никогда не теряют никакой информации. (emph my)

[немного] более длинный ответ заключается в том, что вы добавляете два разных типа: 32-разрядное целое число со знаком и 32-разрядное целое число без знака:

  • домен подписанного 32-битного целого составляет -2147,483,648 (0x80000000) — +2,147,483,647 (0x7FFFFFFF).
  • домен 32-разрядного целого без знака равен 0 (0x00000000) — +4 294 967 295 (0xFFFFFFFF).

Таким образом, типы не совместимы, поскольку int не может содержать никаких произвольных uint, а uint не может содержать произвольных int. Они неявно преобразуются (расширяющееся преобразование в соответствии с требованием 13.1.2, что никакая информация не будет потеряна) к следующему самому большому типу, который может содержать оба: a long в этом случае - подписанное 64-разрядное целое число, которое имеет домен -9,223,372,036,854,775,808 (0x8000000000000000) — +9,223,372,036,854,775,807 (0x7FFFFFFFFFFFFFFF).

Отредактировано для заметок: Как в стороне, Выполнение этого кода:

var x = 1024 + 2048u ;
Console.WriteLine( "'x' is an instance of `{0}`" , x.GetType().FullName ) ;

не дает a long в качестве оригинального плакатного примера. Вместо этого создается следующее:

'x' is an instance of `System.UInt32`

Это из-за постоянного сгибания. Первый элемент в выражении 1024 не имеет суффикса и как таковой является int, а второй элемент в выражении 2048u равен uint в соответствии с правилами:

  • Если литерал не имеет суффикса, он имеет первый из этих типов, в котором его значение могут быть представлены: int, uint, long, ulong.
  • Если литерал суффикс U или U, он имеет первый из этих типов, в котором его значение может быть представлено: uint, ulong.

И поскольку оптимизатор знает, что значения, сумма предварительно вычисляется и оценивается как uint.

Консистенция - это хобгоблин маленьких умов.

Ответ 2

Почему int + int = int и uint + uint = uint, но int + uint = long? Какова мотивация этого решения?

Способ формулирования вопроса подразумевает предположение, что команда разработчиков хотела, чтобы int + uint был длинным, и выбрал правила типа для достижения этой цели. Это предположение неверно.

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

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

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

Исследование этих вопросов привело к настоящему дизайну: арифметические операции определяются как int + int → int, uint + uint → uint, long + long → long, int могут быть преобразованы в long, uint могут быть преобразованы в long и т.д.

Следствием этих решений является то, что при добавлении uint + int разрешение перегрузки выбирает long + long как самое близкое совпадение, а long + long длинное, поэтому uint + int long.

Создание uint + int имеет несколько иное поведение, которое вы считаете более разумным, не было целью дизайна команды вообще, потому что смешивание подписанных и неподписанных значений является первым, редким на практике, а во-вторых, почти всегда ошибкой. Команда дизайнеров могла добавлять специальные случаи для каждой комбинации целых чисел с подписью и без знака, двух, четырех и восьми байтов, а также char, float, double и decimal или любого подмножества этих многих сотен случаев, но который работает против цели простоты.

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

Ответ 3

Это проявление разрешения перегрузки для числовых типов

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

http://msdn.microsoft.com/en-us/library/aa691328(v=vs.71).aspx

Если вы посмотрите на

long operator *(long x, long y);
uint operator *(uint x, uint y);

из этой ссылки, вы видите, что это две возможные перегрузки (пример относится к operator *, но то же самое верно для operator +).

uint неявно преобразуется в long для разрешения перегрузки, как и int.

От uint до long, ulong, float, double или decimal.

От int до long, float, double или decimal.

http://msdn.microsoft.com/en-us/library/aa691282(v=vs.71).aspx

Какова мотивация этого решения?

Вероятно, член команды разработчиков должен ответить на этот аспект. Эрик Липперт, где ты?:-) Обратите внимание, что приведенное ниже рассуждение @Nicolas очень правдоподобно, что оба операнда преобразуются в "самый маленький" тип, который может содержать полный диапазон значений для каждого операнда.

Ответ 4

Я думаю, что поведение компилятора довольно логично и ожидаемо.

В следующем коде:

int i;
int j;
var k = i + j;

Для этой операции существует точная перегрузка, поэтому k - int. Такая же логика применяется при добавлении двух uint, двух byte или того, что у вас есть. Задача компилятора здесь проста, она счастлива, потому что разрешение перегрузки находит точное совпадение. Существует довольно хороший шанс, что человек, пишущий этот код, ожидает, что k будет int и знает, что операция может переполняться при определенных обстоятельствах.

Теперь рассмотрим случай, о котором вы спрашиваете:

uint i;
int j;
var k = i + j;

Что видит компилятор? Ну, он видит операцию, которая не имеет соответствующей перегрузки; нет перегрузки оператора +, которая принимает в качестве двух операндов int и a uint. Таким образом, алгоритм разрешения перегрузки продолжается и пытается найти перегрузку оператора, которая может быть действительной. Это означает, что он должен найти перегрузку, где задействованные типы могут "удерживать" исходные операнды; то есть как i, так и j должны быть неявно конвертируемыми в указанный тип (ы).

Компилятор не может неявно преобразовывать uint в int, потому что такое преобразование не exsit. Он не может неявно преобразовывать int в uint либо потому, что это преобразование также не существует (оба могут вызвать изменение величины). Таким образом, единственным выбором, который он действительно имеет, является выбор первого более широкого типа, который может "удерживать" оба типа операндов, что в данном случае равно long. Если оба операнда неявно преобразованы в long k, будучи long очевидным.

Мотивация такого поведения - это ИМО, чтобы выбрать самый безопасный доступный вариант, а не второй догадываться о сомнительном намерении кодера. Компилятор не может дать обоснованное предположение о том, что ожидает человек, который пишет этот код k. A int? Ну, почему бы не uint? Оба варианта кажутся одинаково плохими. Компилятор выбирает единственный логический путь; безопасный: long. Если кодер хочет, чтобы k был либо int, либо unit, ему нужно явно указать один из операндов.

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

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

Ответ 5

i        int i = 1024;
uint x = 2048;
// Technique #1
    x = x + Convert.ToUInt32(i);    
// Technique #2
    x = x + checked((uint)i);
// Technique #3
    x = x + unchecked((uint) i);
// Technique #4
    x = x + (uint)i;

Ответ 6

Численные правила продвижения для С# свободно основаны на правилах Java и C, которые работают путем идентификации типа, с которым оба операнда могут быть преобразованы, а затем, чтобы результат был одного и того же типа. Я думаю, что такой подход был разумным в 1980-х годах, но более новые языки должны отложить его в сторону одного из них, который рассматривает, как используются значения (например, если я разрабатывал язык, то при задании Int32 i1,i2,i3; Int64 l; компилятор обрабатывал i4=i1+i2+i3; используя 32-битную математику [бросая исключение в случае переполнения], обрабатывал бы l=i1+i2+i3; с помощью 64-разрядной математики.), но правила С# являются тем, что они есть и, похоже, не изменятся.

Следует отметить, что правила продвижения С# по определению всегда выбирают перегрузки, которые считаются "наиболее подходящими" по спецификации языка, но это не значит, что они действительно подходят для любых полезных целей. Например, double f=1111111100/11111111.0f; показалось бы, что он должен дать 100.0, и он будет правильно вычислен, если оба операнда будут продвинуты до double, но вместо этого компилятор преобразует целое число 1111111100 в float, давая 1111111040.0f, а затем выполнит разделение, дающее 99.999992370605469.