← C#: linguagem e decisões de aplicação
02 / 10 · 45 MIN

Referências, nullability e texto

Distingue cópias de valores e referências, aplica guardas de null e escolhe regras de texto que correspondem ao contrato.

O que uma atribuição copia

Uma variável de classe contém uma referência para um objeto. Ao atribuir var other = original, ambas podem passar a referir o mesmo objeto. Alterar um campo desse objeto através de other será observável por original. Atribuir depois outro objeto a other apenas muda essa variável; não redireciona original.

Uma variável de struct contém um valor. Copiar um struct copia os seus campos. Se um campo é int, as cópias podem evoluir separadamente. Se um campo é uma referência para um array, a referência é copiada e o array pode continuar partilhado. Um struct não torna automaticamente imutável tudo o que alcança.

Num plano de rollback, uma cópia só é uma fotografia independente se os dados que podem mudar também estiverem isolados. Antes de usar uma atribuição como backup, identifica os campos que contêm referências e as escritas que o processamento seguinte pode fazer.

Strings: conteúdo imutável, variável atribuível

string é um tipo de referência, mas os seus conteúdos são imutáveis. Em string first = "queued"; string second = first; second += "!";, second recebe o resultado da concatenação. first continua a representar "queued". A imutabilidade do objeto não significa que a variável local não possa receber outra referência.

Para operandos cujo tipo é string, == compara conteúdos. Não é necessário que ambos apontem para a mesma instância. ReferenceEquals faz outra pergunta: estas referências identificam o mesmo objeto? Essa pergunta pode ser útil num diagnóstico de partilha, mas não define a identidade de um código de cliente recebido em mensagens diferentes.

Evita generalizar a semântica de string a todas as classes. Um tipo pode fornecer a sua própria igualdade e a operação escolhida também depende dos tipos usados na expressão. Contratos de igualdade e chaves serão aprofundados com objetos e coleções.

Nullability descreve um contrato

Com Nullable=enable, string exprime a intenção de ter uma referência não nula; string? permite expressar ausência. O compilador acompanha o estado da referência e emite avisos quando o uso pode violar essa intenção. As duas anotações continuam a usar System.String em runtime. É diferente de int?, que é Nullable<int>.

Uma biblioteca antiga, dados externos ou código que ignora avisos ainda podem fornecer null. Mudar a anotação do parâmetro não acrescenta automaticamente uma guarda à entrada do método. A configuração de tratar avisos como erros altera o resultado do build, não transforma a anotação numa verificação de runtime.

O postfix ! diz à análise estática para aceitar o estado como não nulo. Não verifica nem substitui o valor. Se string? token = null, token!.Length continua a desreferenciar null. Usa uma guarda onde o contrato exige rejeição; não escondas um diagnóstico sem uma justificação verificável.

Acesso condicional e os seus limites

customer?.Name só acede a Name se customer existir. Se customer é null, a expressão fornece null. Esta regra não suprime exceções de uma operação feita sobre um recetor existente. Um array com um elemento continua a rejeitar values?[4]; ?[] não transforma um índice inválido em ausência.

Na avaliação booleana, && omite o segundo operando quando o primeiro é false. Isso permite escrever code is not null && code.Length > 0. O operador & aplicado a bool avalia ambos os operandos; não serve como substituição dessa guarda. Do mesmo modo, uma guarda de null não prova que o conteúdo cumpra outros limites.

C#14 permite atribuição condicional em referências, como customer?.Name = BuildName(). Se customer é null, o lado direito nem sequer é avaliado. Isto pode evitar trabalho opcional, mas não confirma que uma atualização obrigatória aconteceu. Quando a aplicação precisa dessa confirmação, usa um ramo explícito e conserva um resultado de sucesso ou falha.

Boxing não é conversão numérica

Guardar um int em object faz boxing: o objeto conserva uma cópia do valor int. Se a variável original passar de 15 para 16, o valor já boxed continua a ser 15. O tipo do valor dentro desse objeto também faz parte do contrato.

Com object value = 15, tentar (long)value pede unboxing como long, mas o objeto contém int. A tentativa lança InvalidCastException. Para esta origem conhecida, long count = (int)value primeiro extrai int e depois alarga esse valor para long. checked não muda o tipo que está boxed.

Esta diferença surge em interfaces antigas baseadas em object, coleções heterogéneas e adaptadores. Antes de converter, confirma se estás perante um valor numérico já tipado, um objeto boxed ou texto. São três operações distintas. Se a origem permite vários tipos, identifica e trata os tipos autorizados; não assumes que todo o objeto que mostra "15" tenha o mesmo tipo interno.

Caso guiado: reconhecer comandos

Um agente recebe códigos de comando ASCII. Para este protocolo, "SYNC", "sync" e "Sync" identificam a mesma operação em todas as máquinas. Ordenar nomes para apresentação ao utilizador tem um objetivo diferente e pode legitimamente depender de regras linguísticas. A cultura do servidor não deve alterar a identidade deste comando.

O método abaixo recebe texto que uma validação anterior já limitou a ASCII quando não é null. Uma mensagem nula é recusada. O conteúdo é comparado explicitamente com OrdinalIgnoreCase. Isso evita depender da cultura atual e preserva a regra de aceitar diferentes maiúsculas. O método decide apenas o reconhecimento; não autentica o emissor nem executa sincronização.

Uma comparação correta não substitui o restante contrato. Se os comandos passarem a admitir Unicode ou espaços exteriores, será necessário definir essas regras. Não acrescentes Trim ou uma normalização generalizada só para fazer um exemplo passar: podes estar a aceitar mensagens que o protocolo rejeitava.

Rever partilha, presença e identidade

Antes de alterar um objeto, pergunta que outras referências o alcançam. Antes de aceder a um membro, identifica o que garante presença e quais erros continuam possíveis quando o recetor existe. Antes de comparar texto, escolhe se a regra é identidade de objeto, conteúdo exato ou igualdade definida pelo protocolo.

Uma falha de produção pode começar com uma suposição pequena: uma atribuição tomada por clone, ! tomado por validação, ?[] tomado por captura de exceções ou uma comparação cultural aplicada a um identificador. Cada uma exige uma correção diferente. Registar essas premissas numa revisão é mais útil do que apenas eliminar o aviso ou a exceção observada.

Resumo: referências copiadas podem partilhar objetos; structs podem conter referências partilhadas; strings são imutáveis; anotações nullable e ! não validam entradas em runtime; unboxing exige compatibilidade; a comparação de texto deve expressar o contrato.

static bool IsSync(string? command)
{
    return command is not null
        && string.Equals(command, "SYNC",
            System.StringComparison.OrdinalIgnoreCase);
}
NA PRÁTICA

Método a ler numa classe com nullable ativado. Assume validação ASCII anterior para valores presentes. null e "" dão false; "Sync" dá true; " SYNC" dá false.

Armadilhas comuns

Confundir atribuição com clone; assumir que struct copia objetos alcançáveis; usar ! para validar null; esperar que ?[] suprima índices inválidos; tentar unboxing com outro tipo numérico.

Tópicos relacionados: Tipos, conversões e limites numéricos · Métodos, parâmetros e padrões · Objetos, interfaces e records · Coleções, generics e igualdade

Leva esta ideia contigo

A cópia, a presença e a igualdade têm contratos distintos; torna cada um explícito na decisão.

Criar conta

Referência: Value types · C# 14 / .NET 10; no preview features