W moim systemie często działają z kodami Airport ( "YYZ"
, "LAX"
, "SFO"
, itp), są one zawsze dokładnie w tym samym formacie (3 litery, reprezentowane jako wielkie litery). System zazwyczaj zajmuje się 25-50 z tych (różnych) kodów na każde żądanie API, z sumą ponad tysiąca alokacji, są one przekazywane przez wiele warstw naszej aplikacji i dość często porównywane.
Zaczęliśmy od przekazywania ciągów znaków, co działało trochę dobrze, ale szybko zauważyliśmy wiele błędów programistycznych, przekazując niewłaściwy kod, gdzie oczekiwano 3-cyfrowego kodu. Natknęliśmy się również na problemy, w których mieliśmy wykonać porównanie bez rozróżniania wielkości liter, a zamiast tego nie wystąpiły błędy, co spowodowało błędy.
Z tego postanowiłem przestać przekazywać ciągi znaków i stworzyć Airport
klasę, która ma jednego konstruktora, który pobiera i sprawdza kod lotniska.
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);
}
}
Dzięki temu nasz kod był znacznie łatwiejszy do zrozumienia i uprościliśmy nasze kontrole równości, słownik / zestaw zastosowań. Wiemy teraz, że jeśli nasze metody zaakceptują Airport
wystąpienie, które będzie zachowywać się tak, jak się spodziewamy, uprościło nasze sprawdzanie metod do zerowego sprawdzania referencji.
Zauważyłem jednak, że zbieranie śmieci działało znacznie częściej, co prześledziłem do wielu przypadków Airport
zbierania.
Moim rozwiązaniem tego było przekonwertowanie class
pliku na struct
. Przeważnie była to tylko zmiana słowa kluczowego, z wyjątkiem GetHashCode
i ToString
:
public override string ToString()
{
return Code ?? string.Empty;
}
public override int GetHashCode()
{
return Code?.GetHashCode() ?? 0;
}
Do obsługi przypadku, w którym default(Airport)
jest używany.
Moje pytania:
Czy tworzenie
Airport
klasy lub struktury było ogólnie dobrym rozwiązaniem, czy też rozwiązuję niewłaściwy problem / rozwiązuję go w niewłaściwy sposób, tworząc typ? Jeśli nie jest to dobre rozwiązanie, jakie jest lepsze rozwiązanie?Jak moja aplikacja powinna obsługiwać instancje, w których
default(Airport)
jest używana? Rodzaj aplikacjidefault(Airport)
jest nonsensowny dla mojej aplikacji, więc robiłem toif (airport == default(Airport) { throw ... }
w miejscach, w których uzyskanie wystąpieniaAirport
(i jegoCode
właściwości) ma kluczowe znaczenie dla operacji.
Uwagi: Przejrzałem pytania dotyczące struktury C # / VB - jak uniknąć przypadku z zerowymi wartościami domyślnymi, które są uważane za nieprawidłowe dla danej struktury? , i użyj struct lub nie przed zadaniem pytania, jednak myślę, że moje pytania są na tyle inne, że uzasadniają swój post.
źródło
default(Airport)
problemu jest po prostu niedozwolone wystąpienia domyślne. Możesz to zrobić, pisząc konstruktor bez parametrów i rzucając goInvalidOperationException
lubNotImplementedException
w nim.Odpowiedzi:
Aktualizacja: Przepisałem swoją odpowiedź, aby odnieść się do niektórych niepoprawnych założeń dotyczących struktur C #, a także OP informującego nas w komentarzach, że używane są internowane łańcuchy.
Jeśli możesz kontrolować dane przychodzące do systemu, skorzystaj z klasy zamieszczonej w pytaniu. Jeśli ktoś uruchomi się
default(Airport)
, odzyskanull
wartość. Pamiętaj, aby napisać swoją prywatnąEquals
metodę zwracającą fałsz przy porównywaniu pustych obiektów lotniska, a następnie pozwólNullReferenceException
, aby latały gdzie indziej w kodzie.Jeśli jednak pobierasz dane do systemu ze źródeł, którymi nie kontrolujesz, nie musisz zawieszać całego wątku. W takim przypadku struct jest idealny, ponieważ prosty fakt
default(Airport)
da ci coś innego niżnull
wskaźnik. Zrób oczywistą wartość reprezentującą „brak wartości” lub „wartość domyślną”, abyś miał coś do wydrukowania na ekranie lub w pliku dziennika (na przykład „---”). W rzeczywistości chciałbym po prostu zachowaćcode
prywatność i w ogóle nie ujawniaćCode
nieruchomości - po prostu skup się na zachowaniu tutaj.W gorszym przypadku konwersja
default(Airport)
na ciąg znaków jest drukowana"---"
i zwraca wartość false w porównaniu z innymi prawidłowymi kodami lotniska. Każdy „domyślny” kod lotniska nie pasuje do niczego, w tym inne domyślne kody lotniska.Tak, struktury mają być wartościami przydzielonymi na stosie, a wszelkie wskaźniki pamięci sterty w zasadzie negują zalety wydajności struktur, ale w tym przypadku domyślna wartość struktury ma znaczenie i zapewnia dodatkową odporność na pociski na resztę podanie.
Z tego powodu zgiąłbym trochę reguły.
Oryginalna odpowiedź (z pewnymi błędami faktycznymi)
Jeśli możesz kontrolować dane przychodzące do twojego systemu, zrobiłbym to, co zasugerował Robert Harvey w komentarzach: Utwórz konstruktora bez parametrów i wyrzuć wyjątek, gdy zostanie wywołany. Zapobiega to przedostawaniu się nieprawidłowych danych do systemu za pośrednictwem
default(Airport)
.Jeśli jednak pobierasz dane do systemu ze źródeł, którymi nie kontrolujesz, nie musisz zawieszać całego wątku. W takim przypadku możesz utworzyć niepoprawny kod lotniska, ale sprawiający wrażenie oczywistego błędu. Wymagałoby to stworzenia konstruktora bez parametrów i ustawienia
Code
czegoś takiego jak „---”:Ponieważ używasz a
string
jako kodu, nie ma sensu używać struct. Struktura jest przydzielana na stosie, tylko w celuCode
przypisania jej jako wskaźnika do łańcucha w pamięci sterty, więc nie ma tutaj różnicy między klasą a strukturą.Jeśli zmienisz kod lotniska na 3-elementową tablicę znaków, struktura zostanie w pełni przydzielona na stosie. Nawet wtedy ilość danych nie jest tak duża, aby coś zmienić.
źródło
Code
właściwości, czy zmieniłoby to twoje uzasadnienie dotyczące tego, że ciąg jest w pamięci sterty?Użyj wzoru Flyweight
Ponieważ lotnisko jest, niezmiennie, niezmienne, nie ma potrzeby tworzenia więcej niż jednego wystąpienia konkretnego, powiedzmy, SFO. Użyj Hashtable lub podobnego (uwaga, jestem facetem Java, nie C #, więc dokładne szczegóły mogą się różnić), aby buforować lotniska po ich utworzeniu. Przed utworzeniem nowego sprawdź w Hashtable. Nigdy nie zwalniasz lotnisk, więc GC nie musi ich zwalniać.
Jedną dodatkową niewielką zaletą (przynajmniej w Javie, nie jestem pewien co do C #) jest to, że nie musisz pisać
equals()
metody, wystarczy proste==
. To samo dotyczyhashcode()
.źródło
getAirportOrCreate()
kod jest poprawnie zsynchronizowany, nie ma technicznego powodu, aby nie tworzyć nowych portów lotniczych w razie potrzeby podczas działania. Mogą istnieć powody biznesowe.Nie jestem szczególnie zaawansowanym programistą, ale czy nie byłoby to idealne zastosowanie dla Enum?
Istnieją różne sposoby konstruowania klas enum z list lub ciągów. Oto jeden, który widziałem w przeszłości, ale nie jestem pewien, czy to najlepszy sposób.
https://blog.kloud.com.au/2016/06/17/converting-webconfig-values-into-enum-or-list/
źródło
Jednym z powodów, dla których widzisz więcej aktywności GC, jest to, że tworzysz teraz drugi ciąg znaków -
.ToUpperInvariant()
wersję oryginalnego ciągu. Oryginalny ciąg kwalifikuje się do GC zaraz po uruchomieniu konstruktora, a drugi kwalifikuje się w tym samym czasie, coAirport
obiekt. Możesz być w stanie zminimalizować go w inny sposób (zwróć uwagę na trzeci parametrstring.Equals()
):źródło
GetHashCode
, powinien po prostu użyćStringComparer.OrdinalIgnoreCase.GetHashCode(Code)
lub podobny