байт + байт = int… почему?

365

Глядя на этот код C #:

byte x = 1;
byte y = 2;
byte z = x + y; // ERROR: Cannot implicitly convert type 'int' to 'byte'

Результат любой математики, выполненной с byte(или short) типами, неявно приводится к целому числу. Решением является явное приведение результата обратно к байту:

byte z = (byte)(x + y); // this works

Что мне интересно, почему? Это архитектурно? Философская?

У нас есть:

  • int+ int=int
  • long+ long=long
  • float+ float=float
  • double+ double=double

Так почему не:

  • byte+ byte=byte
  • short+ short= short?

Немного предыстории: я выполняю длинный список вычислений для «малых чисел» (т.е. <8) и сохраняю промежуточные результаты в большом массиве. Использование байтового массива (вместо массива int) происходит быстрее (из-за попаданий в кэш). Но обширные броски байтов, распространяемые по коду, делают его намного более нечитаемым.

Роберт Картейно
источник
6
stackoverflow.com/questions/927391/…
Тимоти Картер
10
Это не знание Эриком стандарта, которое было бы здесь полезно - это его знание дизайна языка; что не почему. Но да, ответ Эрика был бы довольно определенным :)
Джон Скит
143
Различные рассуждения ниже являются разумным приближением к соображениям дизайна. В более общем смысле: я не думаю о байтах как о «числах»; Я думаю о них как о шаблонах битов, которые можно интерпретировать как числа, или символы, или цвета, или что-то еще. Если вы собираетесь делать с ними математику и рассматривать их как числа, то имеет смысл переместить результат в тип данных, который чаще интерпретируется как число.
Эрик Липперт
28
@Eric: Это имеет большой смысл для байтов, но, вероятно, не так много для коротких / ushort.
Джон Скит
23
@Eric: byte1 | byte2вовсе не рассматривает их как числа. Это относится к ним как к шаблонам битов. Я понимаю вашу точку зрения, но так получилось, что каждый раз, когда я выполнял какие-либо арифметические действия с байтами в C #, я фактически рассматривал их как биты, а не числа, и такое поведение всегда мешает.
Роман Старков

Ответы:

228

Третья строка вашего кода:

byte z = x + y;

на самом деле означает

byte z = (int) x + (int) y;

Таким образом, в байтах нет операции +, байты сначала приводятся к целым числам, а результатом сложения двух целых чисел является (32-разрядное) целое число.

azheglov
источник
Я пробовал код ниже, но он все еще не работает. байт z = (байт) x + (байт) y;
Аноним
10
это потому, что нет операции + для байтов (см. выше). Попробуйте байт z = (byte) ((int) x + (int) y)
ажеглов
35
Это должен быть самый правильный и краткий ответ. Нет операнда для добавления между байтами, поэтому вместо объяснения, почему «добавление двух байтов» работает или нет ( этого никогда не было ), это ясно показывает, почему результатом является int, потому что единственное, что произошло, это добавление 2-х целых ,
RichardTheKiwi
2
У меня закружилась голова при чтении всех остальных ответов (без обид мистера Джона Скита). Я нашел, что это самый простой ответ, который правильно описывает, что происходит под капотом. Спасибо!
Rayryeng
Вот ответ, который я написал в другом месте, в котором содержится программа, позволяющая определить, когда intпроисходит автоматическое продвижение на основе компилятора : stackoverflow.com/a/43578929/4561887
Габриэль Стейплс
172

С точки зрения «почему это вообще происходит», это потому, что в C # нет никаких операторов, определенных для арифметики с байтами, sbyte, short или ushort, как уже говорили другие. Этот ответ о том, почему эти операторы не определены.

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

Я думаю, что это упоминается в одном из аннотированных стандартов C #. Ищу...

РЕДАКТИРОВАТЬ: досадно, я теперь просмотрел аннотированную спецификацию C # 2 ECMA, аннотированную спецификацию MS C # 3 и спецификацию CLI аннотации, и ни один из них не упомянул это, насколько я вижу. Я уверен, что видел причину, приведенную выше, но я взорван, если знаю, где. Извиняюсь, референт фанатов :(

Джон Скит
источник
14
Мне жаль это говорить, но я нахожу это не лучшим ответом.
ВВС
42
Вы опровергли каждый ответ, который вы считаете не лучшим? ;)
Джон Скит
55
(Просто чтобы уточнить, я на самом деле не пытаюсь на вас повлиять. Кажется, у каждого свои критерии для понижения голосов, и это нормально. Я получу отрицательный ответ, только если считаю, что он скорее вреден, чем не идеален. )
Джон Скит
21
Я использую голосование как инструмент, чтобы получить «лучший» ответ на вершину. На самом деле я обнаружил, что вы ничего не сказали в своем ответе, что было главной причиной моего отрицательного голоса. Другой причиной может быть мое субъективное ощущение, что ваш представитель дает вам большой бонус, когда дело доходит до голосования, и вы попадаете на вершину «лучших» ответов.
ВВС
23
ИМО лучший способ получить «лучший» ответ на вершину - это поднять это. Честно говоря, я думаю, что наиболее информативным ответом здесь является комментарий Эрика в вопросе ... но кроме этого, для перспективы дизайна (в отличие от перспективы "что делает компилятор"), я не думаю, что есть много ответ за пределы «производительности». В частности, я действительно не покупаю аргумент «это предотвращает переполнение» (17 голосов), поскольку это предполагает int + int = long.
Джон Скит
68

Я думал, что видел это где-то раньше. Из этой статьи, Старое Новое :

Предположим, мы жили в фантастическом мире, где операции с байтом приводили к байту.

byte b = 32;
byte c = 240;
int i = b + c; // what is i?

В этом фэнтезийном мире ценность меня будет 16! Почему? Поскольку оба операнда оператора + являются байтами, сумма «b + c» вычисляется как байт, что приводит к 16 из-за целочисленного переполнения. (И, как я отмечал ранее, целочисленное переполнение является новым вектором атаки безопасности.)

РЕДАКТИРОВАТЬ : Раймонд, по сути, защищает подход C и C ++ первоначально. В комментариях он защищает тот факт, что C # использует тот же подход на основе обратной совместимости языка.

Майкл Петротта
источник
42
С целыми числами, если мы добавляем их и они переполняются, это не приводит к автоматическому приведению его к другому типу данных, так почему же это делается с байтом?
Райан
2
С int это переполнение. Попробуйте добавить int.MaxValue + 1, вместо 2147483648 вы получите -2147483648.
David Basarab
8
@ Longhorn213: Да, это то, что говорит Райан: int math может переполниться, но int math не возвращает longs.
Майкл Петротта
28
Точно. Если это мера безопасности, то она очень плохо реализована;)
Джон Скит
5
@Ryan: "ленивый" - довольно серьезное обвинение против разработчиков языка C #, за что-то такое же простое, как примитивная математика. Если вы хотите обвинить их в чем-либо, сделайте это «чрезмерной обратной совместимостью с C / C ++».
Майкл Петротта
58

C #

ECMA-334 утверждает, что сложение определено как законное только для int + int, uint + uint, long + long и ulong + ulong (ECMA-334 14.7.4). Как таковые, это возможные операции, которые необходимо учитывать в отношении 14.4.2. Поскольку существуют неявные приведения от байта к int, uint, long и ulong, все члены функции сложения являются применимыми членами функции согласно 14.4.2.1. Мы должны найти лучшее неявное приведение по правилам в 14.4.2.3:

Приведение (C1) к int (T1) лучше, чем приведение (C2) к uint (T2) или ulong (T2), потому что:

  • Если T1 - int, а T2 - uint или ulong, C1 - лучшее преобразование.

Преобразование (C1) в int (T1) лучше, чем приведение (C2) к long (T2), потому что существует неявное приведение от int к long:

  • Если неявное преобразование из T1 в T2 существует, и неявное преобразование из T2 в T1 не существует, C1 - лучшее преобразование.

Следовательно, используется функция int + int, которая возвращает int.

Это очень долгий путь, чтобы сказать, что он очень глубоко скрыт в спецификации C #.

CLI

CLI работает только на 6 типах (int32, native int, int64, F, O и &). (ECMA-335 раздел 3 раздел 1.5)

Байт (int8) не относится к таким типам, и перед добавлением автоматически приводится к типу int32. (ECMA-335 раздел 3 раздел 1.6)

Алан Харфорд
источник
То, что ECMA определяет только эти конкретные операции, не помешает языку реализовать другие правила. VB.NET будет любезно разрешить byte3 = byte1 And byte2без приведения, но бесполезно сгенерирует исключение времени выполнения, если получит int1 = byte1 + byte2значение больше 255. Я не знаю, позволят ли какие-либо языки, byte3 = byte1+byte2и сгенерирует исключение, если оно превысит 255, но не сгенерирует исключение, если int1 = byte1+byte2получит значение в диапазоне 256-510.
суперкат
26

Ответы, указывающие на некоторую неэффективность добавления байтов и усечения результата обратно в байт, неверны. Процессоры x86 имеют инструкции, специально предназначенные для целочисленной работы в 8-битных количествах.

Фактически, для процессоров x86 / 64 выполнение 32-битных или 16-битных операций менее эффективно, чем 64-битных или 8-битных операций из-за байта префикса операнда, который должен быть декодирован. На 32-разрядных компьютерах выполнение 16-разрядных операций влечет за собой то же наказание, но для 8-разрядных операций все еще существуют специальные коды операций.

Многие архитектуры RISC имеют схожие собственные эффективные инструкции. Те, которые, как правило, не имеют длины "хранить и преобразовать в значение со знаком в некотором бите".

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

Кристофер
источник
+1; если бы только это восприятие не было неправильным каждый раз, когда я сдвигался и OR передавал два байта в C # ...
Роман Старков
Не должно быть никаких затрат производительности для усечения результата. В сборке x86 разница только в копировании одного байта из регистра или четырех байтов из регистра.
Джонатан Аллен
1
@JonathanAllen Точно. По иронии судьбы, единственное отличие заключается в расширении конверсии. Текущий дизайн влечет за собой снижение производительности для выполнения увеличивающегося инструкции (либо подписаны продлить или без знака распространяется.)
reirab
« Восприятие того, для чего предназначен тип байта » - это может объяснить это поведение для bytechar), но не для shortкоторого семантически явно число.
Smls
13

Я помню, как однажды читал что-то от Джона Скита (сейчас я не могу его найти, я буду продолжать искать) о том, что байт на самом деле не перегружает оператор +. Фактически, при добавлении двух байтов, как в вашем примере, каждый байт фактически неявно преобразуется в int. Результатом этого, очевидно, является int. Теперь о том, ПОЧЕМУ это было разработано таким образом, я буду ждать, пока сам Джон Скит отправит сообщение :)

РЕДАКТИРОВАТЬ: нашел это! Отличная информация об этой самой теме здесь .

BFree
источник
9

Это из-за переполнения и переноски.

Если вы добавите два 8-битных числа, они могут переполниться в 9-й бит.

Пример:

  1111 1111
+ 0000 0001
-----------
1 0000 0000

Я не знаю наверняка, но я предполагаю, что ints, longsи мне doublesдают больше места, потому что они довольно большие, как есть. Кроме того, они кратны 4, что более эффективно для компьютеров, поскольку ширина внутренней шины данных составляет 4 байта или 32 бита (64 бита становятся все более распространенными в настоящее время). Байт и шорт немного более неэффективны, но они могут сэкономить место.

samoz
источник
23
Но большие типы данных не следуют тому же самому поведению.
Инишир
12
Проблемы переполнения остаются в стороне. Если бы вы взяли свою логику и применили ее к языку, то после добавления арифметики все типы данных возвращали бы больший тип данных, что, безусловно, НЕ имеет место. int + int = int, long + long = long. Я думаю, что вопрос в отношении несоответствия.
Джозеф
Это была моя первая мысль, но тогда почему int + int = long? Так что я не покупаю аргумент "возможного переполнения" ... пока <ухмылка>.
Роберт Картейно
11
О, а насчет аргумента "возможного переполнения", почему бы не byte + byte = short?
Роберт Картейно
А) Почему это работает так, как работает, учитывая правила C #? Смотрите мой ответ ниже. Б) Почему он был разработан так, как есть? Вероятно, это просто соображения удобства использования, основанные на субъективных суждениях о том, как большинство людей склонны использовать целые числа и байты.
Mqp
5

Из спецификации языка C # 1.6.7.5 7.2.6.2 Двоичные числовые продвижения он преобразует оба операнда в int, если не может вписать его в несколько других категорий. Я предполагаю, что они не перегружали оператор + для получения байта в качестве параметра, но хотели, чтобы он работал как обычно, поэтому они просто используют тип данных int.

Спецификация языка C #

Райан
источник
4

Я подозреваю, что C # на самом деле вызывает operator+определенное значение int(которое возвращает intисключение, если вы не в checkedблоке) и неявно приводит оба ваших bytes/ shortsк ints. Вот почему поведение выглядит противоречивым.

MQP
источник
3
Он помещает оба байта в стек, а затем вызывает команду «add». В IL добавьте «съедает» два значения и заменяет их на int.
Джонатан Аллен
3

Вероятно, это было практическое решение со стороны языковых дизайнеров. В конце концов, int - это Int32, 32-разрядное целое число со знаком. Всякий раз, когда вы выполняете целочисленную операцию над типом, меньшим, чем int, он все равно будет преобразован в 32-битное целое число со знаком большинства большинства 32-битных процессоров. Это, в сочетании с вероятностью переполнения маленьких целых чисел, вероятно, заключило сделку. Это избавляет вас от рутинной проверки на предмет избыточного / недостаточного потока, и когда конечный результат выражения в байтах окажется в диапазоне, несмотря на тот факт, что на каком-то промежуточном этапе он будет вне диапазона, вы получите правильный результат.

Другая мысль: переполнение / переполнение этих типов должно быть смоделировано, поскольку оно не будет происходить естественным образом на наиболее вероятных целевых процессорах. Зачем беспокоиться?

PeterAllenWebb
источник
2

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

Все операции с целыми числами, меньшими, чем Int32, округляются до 32 бит до вычисления по умолчанию. Причина, по которой результатом является Int32, заключается в том, чтобы просто оставить его как есть после расчета. Если вы проверяете арифметические коды операций MSIL, то единственными целыми числовыми типами, с которыми они работают, являются Int32 и Int64. Это "по замыслу".

Если вы хотите получить результат обратно в формате Int16, это не имеет значения, если вы выполняете приведение в коде, или компилятор (гипотетически) выдает преобразование «под капотом».

Например, чтобы сделать арифметику Int16:

short a = 2, b = 3;

short c = (short) (a + b);

Два числа расширились бы до 32 битов, были бы добавлены, а затем урезаны до 16 битов, как MS и планировала.

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

Кенан Э.К.
источник
1

Добавление не определено для байтов. Поэтому они приводятся к int для дополнения. Это верно для большинства математических операций и байтов. (обратите внимание, что так было на старых языках, я полагаю, что сегодня это так).

Джим С
источник
0

Я думаю, что это проектное решение о том, какая операция была более распространенной ... Если byte + byte = byte, возможно, гораздо больше людей будет обеспокоено необходимостью приводить к int, когда в качестве результата требуется int.

Фортран
источник
2
На этот раз меня беспокоит другой путь :) Мне всегда нужен результат в байтах, поэтому я всегда должен использовать приведение.
Роман Старков
За исключением того, что вам не нужно приводить к int. Актерский состав неявный. Только другой способ является явным.
Ники
1
@nikie Я думаю, ты не понял мой ответ. Если добавление двух байтов приведет к получению байта, для предотвращения переполнения кому-то придется преобразовать операнды (а не результат) в int перед добавлением.
Фортран
0

Из кода .NET Framework:

// bytes
private static object AddByte(byte Left, byte Right)
{
    short num = (short) (Left + Right);
    if (num > 0xff)
    {
        return num;
    }
    return (byte) num;
}

// shorts (int16)
private static object AddInt16(short Left, short Right)
{
    int num = Left + Right;
    if ((num <= 0x7fff) && (num >= -32768))
    {
        return (short) num;
    }
    return num;
}

Упростите с .NET 3.5 и выше:

public static class Extensions 
{
    public static byte Add(this byte a, byte b)
    {
        return (byte)(a + b);
    }
}

Теперь вы можете сделать:

byte a = 1, b = 2, c;
c = a.Add(b);

Serhio
источник
0

Я проверил производительность между байтом и целым числом.
Со значениями int:

class Program
{
    private int a,b,c,d,e,f;

    public Program()
    {
        a = 1;
        b = 2;
        c = (a + b);
        d = (a - b);
        e = (b / a);
        f = (c * b);
    }

    static void Main(string[] args)
    {
        int max = 10000000;
        DateTime start = DateTime.Now;
        Program[] tab = new Program[max];

        for (int i = 0; i < max; i++)
        {
            tab[i] = new Program();
        }
        DateTime stop = DateTime.Now;

        Debug.WriteLine(stop.Subtract(start).TotalSeconds);
    }
}

С байтовыми значениями:

class Program
{
    private byte a,b,c,d,e,f;

    public Program()
    {
        a = 1;
        b = 2;
        c = (byte)(a + b);
        d = (byte)(a - b);
        e = (byte)(b / a);
        f = (byte)(c * b);
    }

    static void Main(string[] args)
    {
        int max = 10000000;
        DateTime start = DateTime.Now;
        Program[] tab = new Program[max];

        for (int i = 0; i < max; i++)
        {
            tab[i] = new Program();
        }
        DateTime stop = DateTime.Now;

        Debug.WriteLine(stop.Subtract(start).TotalSeconds);
    }
}

Вот результат:
байт: 3,57 с 157 мес., 3,71 с 171 мес., 3,74 с 168 мес. С ЦП ~ = 30%
int: 4,05 с 298 мес., 3,92 с 278 мес., 4,28 294 мес. С ЦП ~ = 27%
Вывод:
байт использует больше ЦП, но это стоит меньше памяти и быстрее (возможно потому, что выделяется меньше байтов)

puipuix
источник
-1

В дополнение ко всем другим замечательным комментариям, я подумал, что добавлю один маленький кусочек. Многие комментарии задавались вопросом, почему int, long и почти любой другой числовой тип также не следуют этому правилу ... возвращают «больший» тип в ответ на арифметику.

Многие ответы были связаны с производительностью (ну, 32 бита быстрее, чем 8 бит). На самом деле, 8-битное число по-прежнему 32-битное число для 32-битного ЦП .... даже если вы добавите два байта, кусок данных, на котором работает процессор, будет 32-битным независимо, поэтому добавление целых не будет Быть "быстрее", чем добавлять два байта ... это все равно для процессора. ТЕПЕРЬ, добавление двух целых будет БЫСТРЕЕ, чем добавление двух длинных на 32-битном процессоре, потому что добавление двух длинных требует больше микроопераций, так как вы работаете с числами шире, чем у процессоров.

Я думаю, что основная причина побуждения байтовой арифметики к целым числам довольно ясна и прямолинейна: 8 бит просто не идут очень далеко! : D С 8 битами у вас есть диапазон без знака 0-255. Это не много места для работы ... вероятность того, что вы столкнетесь с байтовыми ограничениями, ОЧЕНЬ высока при использовании их в арифметике. Однако вероятность того, что у вас закончатся биты при работе с целыми, длинными или двойными и т. Д., Значительно ниже ... настолько мала, что мы очень редко сталкиваемся с необходимостью большего.

Автоматическое преобразование из байта в int логично, потому что масштаб байта очень мал. Автоматическое преобразование из int в long, float в double и т. Д. Не логично, поскольку эти числа имеют значительный масштаб.

jrista
источник
Это все еще не объясняет, почему byte - byteвозвращается int, или почему они не бросают short...
KthProg
Почему вы хотите, чтобы сложение возвращало другой тип, чем вычитание? Если byte + byteвозвращается int, потому что 255 + что-либо больше, чем может содержать байт, не имеет смысла, чтобы какой-либо байт, кроме любого другого байта, возвращал что-либо кроме int с точки зрения согласованности возвращаемого типа.
jrista
Я бы не стал, это просто показывает, что приведенная выше причина, вероятно, не верна. Если бы это было связано с «подгонкой» к результату, то byteвычитание вернуло бы a byte, а сложение байтов вернуло бы short( byte+ byteвсегда будет вписываться в a short). Если бы речь шла о последовательности, как вы говорите, то shortвсе равно было бы достаточно для обеих операций, а не int. Понятно, что существует множество причин, но не все они обязательно продуманы. Или причина производительности, приведенная ниже, может быть более точной.
KthProg