Devo evitar usar int não assinado em C #?

23

Recentemente, pensei no uso de números inteiros não assinados em C # (e acho que argumento semelhante pode ser dito sobre outras "linguagens de alto nível")

Quando Na necessidade de um número inteiro, normalmente não sou confrontado com o dilema do tamanho de um número inteiro, um exemplo seria uma propriedade de idade de uma classe Person (mas a questão não se limita às propriedades). Com isso em mente, há, até onde posso ver, apenas uma vantagem de usar um número inteiro não assinado ("uint") sobre um número inteiro assinado ("int") - legibilidade. Se eu quiser expressar a ideia de que uma idade só pode ser positiva, posso conseguir isso definindo o tipo de idade como uint.

Por outro lado, cálculos em números inteiros não assinados podem levar a todos os tipos de erros e dificulta a execução de operações como subtração de duas idades. (Li que este é um dos motivos pelos quais o Java omitiu números inteiros não assinados)

No caso do C #, também posso pensar que uma cláusula de guarda no levantador seria uma solução que oferece o melhor dos dois mundos, mas isso não seria aplicável quando, por exemplo, uma idade seria passada para algum método. Uma solução alternativa seria definir uma classe chamada Age e ter a propriedade age como a única coisa lá, mas esse padrão faria Eu criar muitas classes e seria uma fonte de confusão (outros desenvolvedores não saberiam quando um objeto é apenas um invólucro e quando é algo mais sofisticado).

Quais são algumas das melhores práticas gerais em relação a esse problema? Como devo lidar com esse tipo de cenário?

Belgi
fonte
1
Além disso, int não assinado não é compatível com CLS, o que significa que você não pode chamar APIs que os usam de outras linguagens .NET.
Nathan Cooper
2
@NathanCooper: ... "não pode chamar APIs que os utilizam de algumas outras línguas". Como os metadados são padronizados, todas as linguagens .NET que suportam tipos não assinados interoperam perfeitamente.
Ben Voigt
5
Para abordar seu exemplo específico, eu não teria uma propriedade chamada Age em primeiro lugar. Eu teria uma propriedade chamada Birthday ou CreationTime ou qualquer outra coisa e calcularia a idade a partir dela.
Eric Lippert
2
"... mas esse padrão Me faria criar muitas classes e seria uma fonte de confusão", na verdade, é a coisa certa a se fazer. Basta procurar o infame anti-padrão da Obsessão Primitiva .
Songo 07/01

Respostas:

24

Os designers do .NET Framework escolheram um número inteiro assinado de 32 bits como seu "número de uso geral" por vários motivos:

  1. Ele pode manipular números negativos, especialmente -1 (que o Framework usa para indicar uma condição de erro; é por isso que um int assinado é usado em todo lugar em que a indexação é necessária, mesmo que números negativos não sejam significativos em um contexto de indexação).
  2. É grande o suficiente para servir à maioria dos propósitos, enquanto é pequeno o suficiente para ser usado economicamente em quase qualquer lugar.

O motivo para usar entradas não assinadas não é a legibilidade; está tendo a capacidade de obter a matemática que somente um int não assinado fornece.

Cláusulas de guarda, validação e pré-condições do contrato são formas perfeitamente aceitáveis ​​de garantir intervalos numéricos válidos. Raramente um intervalo numérico do mundo real corresponde exatamente a um número entre zero e 2 32 -1 (ou qualquer que seja o intervalo numérico nativo do tipo numérico que você escolheu), portanto, usar a uintpara restringir seu contrato de interface a números positivos é uma espécie de além do ponto.

Robert Harvey
fonte
2
Boa resposta! Além disso, pode haver alguns casos em que um int não assinado pode, na verdade, inadvertidamente produzir mais erros (embora provavelmente sejam imediatamente identificados, mas um pouco confusos) - imagine fazer um loop inverso com um contador int não assinado porque algum tamanho é um número inteiro: for (uint j=some_size-1; j >= 0; --j)- whoops ( não tenho certeza se esse é um problema em C #)! Eu encontrei esse problema no código antes, no qual tentamos usar int não assinado no lado C, tanto quanto possível - e acabamos mudando para favorecer intmais tarde, e nossas vidas eram muito mais fáceis com menos avisos do compilador.
14
"Raramente um intervalo numérico do mundo real corresponde a um número entre zero e 2 ^ 32-1." Na minha experiência, se você precisar de um número maior que 2 ^ 31, é provável que também precise de números maiores que 2 ^ 32, portanto, você também pode passar para int64 (assinado) em esse ponto.
Mason Wheeler
3
@ Panzercrisis: Isso é um pouco grave. Provavelmente seria mais preciso dizer "Use a intmaior parte do tempo, porque essa é a convenção estabelecida, e é o que a maioria das pessoas espera que seja usada rotineiramente. Use uintquando você precisar dos recursos especiais de a uint". Lembre-se de que os designers do Framework decidiram seguir essa convenção extensivamente, então você não pode nem usá-lo uintem muitos contextos do Framework (não é compatível com o tipo).
Robert Harvey
2
@ Panzercrisis Pode ser uma frase muito forte; mas não tenho certeza se alguma vez usei tipos não assinados em c #, exceto quando estava ligando para win32 apis (onde a convenção é que constantes / flags / etc não são assinadas).
Dan Neely
4
É realmente muito raro. A única vez que utilizo ints não assinados é em cenários de manipulação de bits.
Robert Harvey
8

Geralmente, você deve sempre usar o tipo de dados mais específico possível para os seus dados.

Se, por exemplo, você estiver usando o Entity Framework para extrair dados de um banco de dados, o EF usará automaticamente o tipo de dados mais próximo do usado no banco de dados.

Há dois problemas com isso em c #.
Primeiro, a maioria dos desenvolvedores de C # usa apenas intpara representar números inteiros (a menos que haja um motivo para usá-lo long). Isso significa que outros desenvolvedores não pensarão em verificar o tipo de dados e, portanto, receberão os erros de estouro mencionados acima. A segunda, e assunto mais crítico, é / era que do .NET operadores aritméticos originais suportado apenas int, uint, long, ulong, float, duplos, e decimal*. Esse ainda é o caso hoje (consulte a seção 7.8.4 na especificação de idioma do C # 5.0 ). Você pode testar isso usando o seguinte código:

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;

O resultado do nosso byte- byteé um int( System.Int32).

Esses dois problemas deram origem à prática do "único uso int para números inteiros", que é tão comum.

Portanto, para responder sua pergunta, em C # geralmente é uma boa ideia manter-se, a intmenos que:

  • Um gerador de código automatizado usava um valor diferente (como o Entity Framework).
  • Todos os outros desenvolvedores do projeto sabem que você está usando os tipos de dados menos comuns (inclua um comentário indicando que você usou o tipo de dados e por quê).
  • Os tipos de dados menos comuns já são usados ​​no projeto.
  • O programa requer os benefícios do tipo de dados menos comum (você tem 100 milhões deles que precisa manter na RAM, portanto a diferença entre a bytee um intou um inte a e a longé crítica, ou as diferenças aritméticas dos não assinados já mencionados).

Se você precisar fazer cálculos nos dados, siga os tipos comuns.
Lembre-se, você pode transmitir de um tipo para outro. Isso pode ser menos eficiente do ponto de vista da CPU, então você provavelmente está melhor com um dos sete tipos comuns, mas é uma opção, se necessário.

Enumerações ( enum) é uma das minhas exceções pessoais às diretrizes acima. Se eu tiver apenas algumas opções, especificarei a enumeração como um byte ou um curto. Se eu precisar desse último bit em uma enum sinalizada, especificarei o tipo a ser uintusado para que eu possa usar hex para definir o valor da sinalização.

Se você usar uma propriedade com código de restrição de valor, não deixe de explicar na tag de resumo quais restrições existem e por quê.

* Aliases de C # são usados ​​em vez de nomes de .NET, como se System.Int32trata de uma pergunta de C #.

Nota: havia um blog ou artigo dos desenvolvedores .NET (que não consigo encontrar), que apontava o número limitado de funções aritméticas e algumas razões pelas quais eles não se preocupavam. Pelo que me lembro, eles indicaram que não tinham planos de adicionar suporte para os outros tipos de dados.

Nota: Java não suporta tipos de dados não assinados e anteriormente não tinha suporte para números inteiros de 8 ou 16 bits. Como muitos desenvolvedores de C # vieram de Java ou precisavam trabalhar em ambas as linguagens, as limitações de uma linguagem às vezes seriam artificialmente impostas à outra.

Trisped
fonte
Minha regra geral é simplesmente "use int, a menos que você não possa".
precisa saber é
@PerryC Eu acredito que essa é a convenção mais comum. O objetivo da minha resposta foi fornecer uma convenção mais completa que permita o uso dos recursos de idioma.
Trisped
6

Você precisa estar ciente principalmente de duas coisas: os dados que está representando e quaisquer etapas intermediárias em seus cálculos.

Certamente faz sentido ter idade unsigned int, porque geralmente não consideramos idades negativas. Mas então você menciona subtrair uma idade de outra. Se apenas subtrairmos cegamente um número inteiro de outro, é definitivamente possível acabar com um número negativo, mesmo que tenhamos concordado anteriormente que idades negativas não fazem sentido. Portanto, nesse caso, você deseja que seu cálculo seja feito com um número inteiro assinado.

Quanto a saber se os valores não assinados são ruins ou não, eu diria que é uma generalização enorme dizer que os valores não assinados são ruins. Java não possui valores não assinados, como você mencionou, e isso constantemente me incomoda. A bytepode ter um valor de 0-255 ou 0x00-0xFF. Mas se você deseja instanciar um byte maior que 127 (0x7F), é necessário escrevê-lo como um número negativo ou converter um número inteiro em um byte. Você acaba com um código parecido com este:

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

O exposto acima me irrita sem fim. Não tenho permissão para que um byte tenha um valor de 197, mesmo que seja um valor perfeitamente válido para a maioria das pessoas sãs que lidam com bytes. Posso converter o número inteiro ou encontrar o valor negativo (197 == -59 neste caso). Considere também isso:

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

Como você pode ver, adicionar dois bytes com valores válidos e terminar com um byte com um valor válido acaba alterando o sinal. Não apenas isso, mas não é imediatamente óbvio que 70 + 80 == -106. Tecnicamente, isso é um estouro, mas, na minha opinião (como ser humano), um byte não deve estourar para valores abaixo de 0xFF. Quando faço bit aritmético no papel, não considero o oitavo bit um sinal.

Eu trabalho com muitos números inteiros no nível de bits, e ter tudo assinado normalmente torna tudo menos intuitivo e mais difícil de lidar, porque você deve se lembrar que mudar um número negativo à direita dá a você novos 1s no seu número. Enquanto o deslocamento à direita de um número inteiro sem sinal nunca faz isso. Por exemplo:

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;

Apenas adiciona etapas extras que considero não necessárias.

Enquanto eu usei byteacima, o mesmo se aplica aos números inteiros de 32 e 64 bits. Não ter unsignedé incapacitante e me choca o fato de existirem linguagens de alto nível, como Java, que não as permitem. Mas para a maioria das pessoas isso não é problema, porque muitos programadores não lidam com aritmética em nível de bit.

No final, é útil usar números inteiros não assinados se você estiver pensando neles como bits, e é útil usar números inteiros assinados quando estiver pensando neles como números.

Shaz
fonte
7
Compartilho sua frustração sobre idiomas sem tipos integrais não assinados (especialmente para bytes), mas receio que essa não seja uma resposta direta à pergunta feita aqui. Talvez você poderia adicionar uma conclusão, que eu acredito, poderia ser: “Usar inteiros sem sinal, se você está pensando de seu valor como bits e assinados inteiros se você está pensando sobre eles como números.”
5gon12eder
1
é o que eu disse em um comentário acima. feliz em ver alguém pensando da mesma maneira.
22816 Robert Bristow-Johnson