Стоит ли избегать использования unsigned int в C #?

23

Недавно я подумал об использовании целых чисел без знака в C # (и я думаю, что аналогичный аргумент можно сказать о других "языках высокого уровня")

Когда я нуждаюсь в целом числе, я обычно не сталкиваюсь с дилеммой размера целого числа, примером может служить свойство age класса Person (но вопрос не ограничивается свойствами). Имея это в виду, насколько я вижу, есть только одно преимущество использования целого без знака («uint») над целым числом со знаком («int») - читаемость. Если я хочу выразить идею о том, что возраст может быть только положительным, я могу достичь этого, установив тип возраста в uint.

С другой стороны, вычисления на целых числах без знака могут привести к ошибкам всех видов, что затрудняет выполнение таких операций, как вычитание двух возрастов. (Я читал, что это одна из причин, почему Java пропустил целые числа без знака)

В случае C # я также могу подумать, что защитное предложение для установщика было бы решением, которое дает лучшее из двух миров, но это не будет применимо, когда, например, для некоторого метода будет задан возраст. Обходной путь должен был бы определить класс с именем Age, и свойство age было бы единственным, но этот шаблон заставил бы меня создавать много классов и был бы источником путаницы (другие разработчики не знали бы, когда объект является просто оболочкой). и когда это что-то более изощренное).

Каковы общие рекомендации по этому вопросу? Как я должен иметь дело с этим типом сценария?

Белги
источник
1
Кроме того, unsigned int не совместим с CLS, что означает, что вы не можете вызывать API, использующие их, из других языков .NET.
Натан Купер
2
@NathanCooper: ... «не может вызывать API - интерфейсы , которые используют их из некоторых других языков». Метаданные для них стандартизированы, поэтому все языки .NET, которые поддерживают неподписанные типы, будут нормально взаимодействовать.
Бен Фойгт
5
Если обратиться к вашему конкретному примеру, у меня не было бы свойства с именем Age. Я хотел бы иметь свойство с именем Birthday или CreationTime или чем-то еще, и вычислять возраст по нему.
Эрик Липперт
2
«... но этот паттерн заставил бы Меня создать много классов и стал бы источником путаницы», на самом деле это правильно. Просто найдите печально известный анти-шаблон Примитивная одержимость .
Сонго

Ответы:

24

Разработчики .NET Framework выбрали 32-разрядное целое число со знаком в качестве «общего числа» по нескольким причинам:

  1. Он может обрабатывать отрицательные числа, особенно -1 (который Framework использует для обозначения состояния ошибки; именно поэтому подписанное int используется везде, где требуется индексация, даже если отрицательные числа не имеют смысла в контексте индексации).
  2. Он достаточно большой, чтобы служить большинству целей, и при этом достаточно мал, чтобы его можно было использовать практически везде.

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

Охранные положения, проверка и предварительные условия контракта являются вполне приемлемыми способами обеспечения допустимых числовых диапазонов. Редко когда реальный числовой диапазон соответствует в точности номеру от 0 до 2 32 -1 (или любой другой родной числовой диапазон того типа, который вы выбрали), поэтому использование uintограничения интерфейса вашего контракта на положительные числа является своего рода не в этом дело.

Роберт Харви
источник
2
Хороший ответ! Также могут быть случаи, когда беззнаковое целое может на самом деле непреднамеренно вызвать больше ошибок (хотя, вероятно, сразу обнаруженных, но немного сбивающих с толку) - представьте зацикливание в обратном порядке со счетчиком целого без знака, потому что некоторый размер является целым числом: for (uint j=some_size-1; j >= 0; --j)- whoops ( не уверен, если это проблема в C #)! Я обнаружил эту проблему в коде, который до этого пытался максимально использовать unsigned int на стороне C - и в итоге мы изменили его на intболее благоприятный , и наша жизнь стала намного проще с меньшим количеством предупреждений компилятора.
14
«Редко, когда реальный числовой диапазон соответствует числу от нуля до 2 ^ 32-1». По моему опыту, если вам понадобится число больше 2 ^ 31, вам, скорее всего, понадобятся числа больше 2 ^ 32, так что вы можете просто перейти на (подписанный) int64 в этот момент.
Мейсон Уилер
3
@Panzercrisis: Это немного серьезно. Вероятно, было бы более точным сказать: «Используйте intбольшую часть времени, потому что это установленное соглашение, и это то, что большинство людей ожидают увидеть в повседневном использовании. Используйте, uintкогда вам требуются специальные возможности a uint». Помните, что разработчики Framework решили тщательно следовать этому соглашению, поэтому вы даже не можете использовать его uintво многих контекстах Framework (он не совместим с типами).
Роберт Харви
2
@Panzercrisis Это может быть слишком сильное выражение; но я не уверен, что когда-либо использовал неподписанные типы в C #, за исключением тех случаев, когда я вызывал apis win32 (где принято, что константы / флаги / и т. д. не подписаны).
Дэн Нили
4
Это действительно довольно редко. Единственный раз, когда я использую целые числа без знака, это сценарии с битами.
Роберт Харви
8

Как правило, вы всегда должны использовать наиболее конкретный тип данных для ваших данных.

Например, если вы используете Entity Framework для извлечения данных из базы данных, EF автоматически использует тип данных, ближайший к тому, который используется в базе данных.

Есть две проблемы с этим в C #.
Во-первых, большинство разработчиков на C # используют только intдля представления целых чисел (если нет причин использовать long). Это означает, что другие разработчики не будут думать о проверке типа данных, поэтому они получат ошибки переполнения, упомянутые выше. Во - вторых, и более важным вопросом, является / в том , что .NET в исходные арифметические операторы поддерживаются только int, uint, long, ulong, float, двойной, и decimal*. Это все еще актуально сегодня (см. Раздел 7.8.4 в спецификации языка C # 5.0 ). Вы можете проверить это самостоятельно, используя следующий код:

byte a, b;
a = 1;
b = 2;
var c = a - b;      //In visual studio, hover over "var" and the tip will indicate the data type, or you can get the value from cName below.
string cName = c.GetType().Namespace + '.' + c.GetType().Name;

Результат нашего byte- byteэто int(System.Int32 ).

Эти два вопроса привели к распространенной практике «единственное использование целых чисел».

Поэтому, чтобы ответить на ваш вопрос, в C # обычно стоит придерживаться следующих intправил:

  • Автоматический генератор кода использовал другое значение (например, Entity Framework).
  • Все остальные разработчики проекта знают, что вы используете менее распространенные типы данных (включая комментарий, указывающий, что вы использовали тип данных и почему).
  • Менее распространенные типы данных обычно уже используются в проекте.
  • Программа требует преимуществ менее распространенного типа данных (у вас есть 100 миллионов таких данных, которые необходимо хранить в оперативной памяти, поэтому разница между a byteи a intили a intи a longявляется критической, или арифметические различия без знака уже упоминались).

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

Enumerations ( enum) - одно из моих личных исключений из приведенных выше рекомендаций. Если у меня есть только несколько вариантов, я укажу перечисление в байтах или коротких. Если мне понадобится последний бит во помеченном перечислении, я укажу тип, который должен быть, uintчтобы я мог использовать hex для установки значения для флага.

Если вы используете свойство с кодом, ограничивающим значение, обязательно объясните в итоговом теге, какие существуют ограничения и почему.

Псевдонимы * C # используются вместо имен .NET, например, System.Int32так как это вопрос C #.

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

Примечание. Java не поддерживает типы данных без знака и ранее не поддерживала 8- или 16-битные целые числа. Поскольку многие разработчики на C # пришли из Java-опыта или должны были работать на обоих языках, ограничения одного языка иногда искусственно накладывались на другой.

Trisped
источник
Мое эмпирическое правило простое: «используйте int, если вы не можете».
PerryC
@PerryC Я считаю, что это наиболее распространенное соглашение. Суть моего ответа заключалась в том, чтобы предоставить более полное соглашение, которое позволяет вам использовать языковые функции.
Trisped
6

В основном вам необходимо знать две вещи: данные, которые вы представляете, и любые промежуточные этапы ваших расчетов.

Конечно, имеет смысл иметь возраст unsigned int , потому что мы обычно не учитываем отрицательный возраст. Но тогда вы упоминаете вычитание одного возраста из другого. Если мы просто слепо вычтем одно целое число из другого, то определенно можно получить отрицательное число, даже если мы ранее согласились с тем, что отрицательный возраст не имеет смысла. Так что в этом случае вы хотите, чтобы ваш расчет был выполнен со знаком целого числа.

Относительно того, являются ли неподписанные значения плохими или нет, я бы сказал, что обобщение того, что неподписанные значения являются плохими, является огромным обобщением. Как вы упоминали, в Java нет значений без знака, и это постоянно раздражает меня. A byteможет иметь значение от 0 до 255 или 0x00-0xFF. Но если вы хотите создать экземпляр байта больше 127 (0x7F), вы должны либо записать его как отрицательное число, либо привести целое число к байту. Вы получите код, который выглядит следующим образом:

byte a = 0x80; // Won't compile!
byte b = (byte) 0x80;
byte c = -128; // Equal to b

Вышесказанное раздражает меня до бесконечности. Мне не разрешают иметь значение байта 197, хотя это вполне допустимое значение для большинства здравомыслящих людей, имеющих дело с байтами. Я могу привести целое число или найти отрицательное значение (в данном случае 197 == -59). Также учтите это:

byte a = 70;
byte b = 80;
byte c = a + b; // c == -106

Итак, как вы можете видеть, добавление двух байтов с допустимыми значениями и получение байта с действительным значением приводит к изменению знака. Не только это, но не сразу очевидно, что 70 + 80 == -106. Технически это переполнение, но, по моему мнению (как человеческое существо), байт не должен переполняться для значений ниже 0xFF. Когда я делаю битовую арифметику на бумаге, я не считаю 8-й бит знаковым битом.

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

signed byte b = 0b10000000;
b = b >> 1; // b == 0b1100 0000
b = b & 0x7F;// b == 0b0100 0000

unsigned byte b = 0b10000000;
b = b >> 1; // b == 0b0100 0000;

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

Хотя я использовал byteвыше, то же самое относится к 32-разрядным и 64-разрядным целым числам. Отсутствие unsignedвредно для меня, и это шокирует меня тем, что существуют языки высокого уровня, такие как Java, которые вообще их не допускают. Но для большинства людей это не проблема, потому что многие программисты не имеют дело с арифметикой на уровне битов.

В конце концов, полезно использовать целые числа без знака, если вы думаете о них как о битах, и полезно использовать целые числа со знаком, когда вы думаете о них как о числах.

Shaz
источник
7
Я разделяю ваше разочарование по поводу языков без целочисленных типов без знака (особенно для байтов), но я боюсь, что это не прямой ответ на вопрос, заданный здесь. Возможно, вы могли бы добавить заключение, которое, как я считаю, может быть следующим: «Используйте целые числа без знака, если вы думаете об их значении как биты, и целые числа со
знаком,
1
это то, что я сказал в комментарии выше. рад видеть кого-то другого, думающего так же.
Роберт Бристоу-Джонсон