2014-10-14 5 views
22

Я удивлен C# поведения компилятора в следующем примере:Добавление междунар и 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?

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

+0

Ознакомьтесь с ответом Эрика Липперта ниже. Он был фактически вовлечен в дизайн C#, поэтому его единственный ответ здесь - на «мотивацию для этого решения», который является авторитетным. –

+0

Да, это действительно здорово, чтобы получить ответ от Эрика Липперта. –

ответ

11

Короткий ответ «потому что стандарт говорит, что это будет так», что увидеть информативного § 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 шахта)

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

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

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

Edited отметить: Подобно тому, как в сторону, Исполнительное этот код:

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

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

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

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

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

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

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

+0

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

+0

Несмотря на то, что Эрик Липперт высказал свое мнение, я думаю, что это лучше и более убедительно. Хотя +1 для обоих объяснений. –

+0

@AntonK ... хорошо, geez. Благодарю. [blushes] –

12

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

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

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

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

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

+3

Я здесь. Я добавил ответ. –

+0

@EricLippert: Рад, что вы смогли это сделать :-) –

2
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; 
+0

Что такое мотивация? – Mark

+0

Число можно манипулировать, но нормализации нет, здесь я просто проиллюстрировал выход, образно, банан (int) - это плод (длинный), яблоко (uint) - плод (длинный), банан не яблоко –

+1

Вы могли бы хотите добавить свою мотивацию к своему ответу (а не в комментарии). – Mark

5

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

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

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

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

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

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

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

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

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

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

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

+0

Ответ хороший, но я хочу уточнить, что преобразование из int в uint не плохо, потому что оно теряет * информацию *. Он не теряет никакой информации; После преобразования сохраняется 100% бит. Вы можете совершить кругосветное путешествие от int до uint и обратно без потери информации. Проблема заключается в изменении * величины *. Значение int -1 становится значением uint четыре миллиарда и что-то, массовое изменение. –

+0

@ ЭрикЛипперт кричит, исправил это. Спасибо! – InBetween

2

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

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

14

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

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

Скорее, проектная группа мысль:

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

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

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

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

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

+1

Если программист действительно требует одного из «редких на практике» действий (и знает, что фактические значения не вызовут ошибки, такие как переполнение целого), они всегда могут заставить другое разрешение перегрузки посредством явного каста, правильно? –

+0

@EricJ .: Точно. Приведение может рассматриваться как разработчик, дающий разъяснение компилятору: Нет, * действительно *, я имею в виду рассматривать эту вещь как целое число со знаком, даже если это не так. –

Смежные вопросы