No meu sistema eu freqüentemente operar com códigos de aeroporto ( "YYZ"
, "LAX"
, "SFO"
, etc.), eles estão sempre no mesmo formato exato (3 letras, representado como maiúsculas). O sistema normalmente lida com 25 a 50 desses códigos (diferentes) por solicitação da API, com mais de mil alocações no total, eles são passados por várias camadas de nosso aplicativo e são comparados quanto à igualdade com bastante frequência.
Começamos apenas passando as strings, o que funcionou bem por um tempo, mas rapidamente notamos muitos erros de programação, passando um código errado em algum lugar em que o código de 3 dígitos era esperado. Também enfrentamos problemas em que deveríamos fazer uma comparação sem distinção entre maiúsculas e minúsculas e, em vez disso, não resultando em erros.
A partir disso, decidi parar de passar as strings e criar uma Airport
classe, que tem um único construtor que pega e valida o código do aeroporto.
public sealed class Airport
{
public Airport(string code)
{
if (code == null)
{
throw new ArgumentNullException(nameof(code));
}
if (code.Length != 3 || !char.IsLetter(code[0])
|| !char.IsLetter(code[1]) || !char.IsLetter(code[2]))
{
throw new ArgumentException(
"Must be a 3 letter airport code.",
nameof(code));
}
Code = code.ToUpperInvariant();
}
public string Code { get; }
public override string ToString()
{
return Code;
}
private bool Equals(Airport other)
{
return string.Equals(Code, other.Code);
}
public override bool Equals(object obj)
{
return obj is Airport airport && Equals(airport);
}
public override int GetHashCode()
{
return Code?.GetHashCode() ?? 0;
}
public static bool operator ==(Airport left, Airport right)
{
return Equals(left, right);
}
public static bool operator !=(Airport left, Airport right)
{
return !Equals(left, right);
}
}
Isso tornou nosso código muito mais fácil de entender e simplificamos nossas verificações de igualdade, uso de dicionário / conjunto. Agora sabemos que, se nossos métodos aceitarem uma Airport
instância que se comportará da maneira que esperamos, simplificou nossas verificações de método para uma verificação de referência nula.
O que notei, no entanto, foi que a coleta de lixo estava sendo executada com muito mais frequência, que eu rastreei para muitas instâncias de Airport
coleta.
Minha solução para isso foi converter o arquivo class
em struct
. Principalmente foi apenas uma alteração de palavra-chave, com exceção de GetHashCode
e ToString
:
public override string ToString()
{
return Code ?? string.Empty;
}
public override int GetHashCode()
{
return Code?.GetHashCode() ?? 0;
}
Para lidar com o caso em que default(Airport)
é usado.
Minhas perguntas:
A criação de uma
Airport
classe ou estrutura foi uma boa solução em geral, ou estou resolvendo o problema errado / resolvendo-o da maneira errada, criando o tipo? Se não é uma boa solução, qual é a melhor solução?Como meu aplicativo deve lidar com instâncias em que o
default(Airport)
arquivo é usado? Um tipo dedefault(Airport)
é absurdo para o meu aplicativo, então eu tenho feitoif (airport == default(Airport) { throw ... }
em lugares onde obter uma instância deAirport
(e suaCode
propriedade) é fundamental para a operação.
Notas: Revisei as perguntas C # / VB struct - como evitar casos com zero valor padrão, que é considerado inválido para determinada estrutura? , e Use struct ou não antes de fazer minha pergunta, no entanto, acho que minhas perguntas são diferentes o suficiente para garantir sua própria postagem.
fonte
default(Airport)
problema é simplesmente desautorizar as instâncias padrão. Você pode fazer isso escrevendo um construtor sem parâmetros e jogandoInvalidOperationException
ouNotImplementedException
nele.Respostas:
Atualização: reescrevi minha resposta para abordar algumas suposições incorretas sobre estruturas C #, bem como o OP nos informando nos comentários que as cadeias internas estão sendo usadas.
Se você pode controlar os dados que chegam ao seu sistema, use uma classe conforme publicado na sua pergunta. Se alguém correr
default(Airport)
, receberá umnull
valor de volta. Certifique-se de escrever seuEquals
método privado para retornar false sempre que comparar objetos nulos do Airport e, em seguida, deixe osNullReferenceException
voarem para outro lugar no código.No entanto, se você estiver levando dados para o sistema a partir de fontes que não controla, não será necessário travar todo o encadeamento. Nesse caso, uma estrutura é ideal, pois o fato simples
default(Airport)
fornecerá algo diferente de umnull
ponteiro. Crie um valor óbvio para representar "sem valor" ou o "valor padrão" para ter algo para imprimir na tela ou em um arquivo de log (como "---", por exemplo). Na verdade, eu apenas manteria ocode
privado e não exporia umaCode
propriedade - apenas me focaria no comportamento aqui.No pior cenário possível, a conversão
default(Airport)
em uma seqüência de caracteres é impressa"---"
e retorna false quando comparado a outros códigos de aeroporto válidos. Qualquer código de aeroporto "padrão" não corresponde a nada, incluindo outros códigos de aeroporto padrão.Sim, as estruturas devem ser valores alocados na pilha, e qualquer ponteiro para acumular memória basicamente nega as vantagens de desempenho das estruturas, mas, nesse caso, o valor padrão de uma estrutura tem significado e fornece alguma resistência a marcadores adicionais para o restante do inscrição.
Eu dobraria as regras um pouco aqui, por causa disso.
Resposta original (com alguns erros factuais)
Se você pode controlar os dados que chegam ao seu sistema, eu faria o que Robert Harvey sugeriu nos comentários: Crie um construtor sem parâmetros e lance uma exceção quando for chamada. Isso impede que dados inválidos entrem no sistema via
default(Airport)
.No entanto, se você estiver levando dados para o sistema a partir de fontes que você não controla, não será necessário travar todo o encadeamento. Nesse caso, você pode criar um código de aeroporto inválido, mas parecer um erro óbvio. Isso envolveria a criação de um construtor sem parâmetros e a configuração
Code
para algo como "---":Como você está usando a
string
como o código, não faz sentido usar uma estrutura. A estrutura é alocada na pilha, apenas paraCode
alocá-la como ponteiro para uma seqüência de caracteres na memória heap, portanto, não há diferença aqui entre classe e estrutura.Se você alterasse o código do aeroporto para uma matriz de 3 itens de caracteres, uma estrutura seria totalmente alocada na pilha. Mesmo assim, o volume de dados não é tão grande para fazer a diferença.
fonte
Code
propriedade, isso mudaria sua justificativa em relação ao fato de o ponto da sequência estar na memória de pilha?Use o padrão Flyweight
Como o Airport é, corretamente, imutável, não há necessidade de criar mais de uma instância de uma determinada, por exemplo, SFO. Use um Hashtable ou similar (observe, eu sou um cara Java, não C #, para que detalhes exatos possam variar), para armazenar em cache os aeroportos quando eles forem criados. Antes de criar um novo, verifique no Hashtable. Você nunca está liberando aeroportos, portanto o GC não precisa liberá-los.
Uma pequena vantagem adicional (pelo menos em Java, não tenho certeza sobre C #) é que você não precisa escrever um
equals()
método,==
basta fazer isso. O mesmo parahashcode()
.fonte
getAirportOrCreate()
código estiver sincronizado corretamente, não há motivo técnico para você não criar novos aeroportos conforme necessário durante o tempo de execução. Pode haver razões comerciais.Eu não sou um programador particularmente avançado, mas isso não seria um uso perfeito para um Enum?
Existem diferentes maneiras de construir classes enum a partir de listas ou seqüências de caracteres. Aqui está um que eu vi no passado, mas não tenho certeza se é o melhor caminho.
https://blog.kloud.com.au/2016/06/17/converting-webconfig-values-into-enum-or-list/
fonte
Um dos motivos pelos quais você está vendo mais atividades no GC é porque está criando uma segunda sequência agora - a
.ToUpperInvariant()
versão da sequência original. A sequência original é elegível para o GC logo após a execução do construtor e a segunda é elegível ao mesmo tempo que oAirport
objeto. Você pode minimizá-lo de uma maneira diferente (observe o terceiro parâmetro emstring.Equals()
):fonte
GetHashCode
, deve apenas usarStringComparer.OrdinalIgnoreCase.GetHashCode(Code)
ou similar