В моей системе я часто работаю с кодами аэропортов ( "YYZ"
, "LAX"
, "SFO"
и т.д.), они всегда находятся в том же формате (3 письма, представленный в верхнем регистре). Система обычно имеет дело с 25-50 из этих (разных) кодов на запрос API, с общим количеством распределений более тысячи, они проходят через многие уровни нашего приложения и довольно часто сравниваются на равенство.
Мы начали с простой передачи строк, которые немного подействовали, но мы быстро заметили множество ошибок в программировании, передав неправильный код, где ожидался трехзначный код. Мы также столкнулись с проблемами, при которых мы должны были делать сравнение без учета регистра, а не делать этого, что приводило к ошибкам.
Исходя из этого, я решил прекратить передачу строк и создать Airport
класс, который имеет единственный конструктор, который принимает и проверяет код аэропорта.
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);
}
}
Это сделало наш код намного проще для понимания, и мы упростили наши проверки на равенство, использование словаря / набора. Теперь мы знаем, что если наши методы принимают Airport
экземпляр, который будет вести себя так, как мы ожидаем, он упростит наши проверки методов до проверки нулевых ссылок.
Однако я заметил, что сборка мусора выполнялась гораздо чаще, и я проследил это до многих случаев Airport
сбора.
Мое решение для этого было преобразовать class
в struct
. В основном это было просто изменение ключевого слова, за исключением GetHashCode
и ToString
:
public override string ToString()
{
return Code ?? string.Empty;
}
public override int GetHashCode()
{
return Code?.GetHashCode() ?? 0;
}
Для обработки случая, где default(Airport)
используется.
Мои вопросы:
Было ли создание
Airport
класса или структуры хорошим решением в целом, или я решил неправильную проблему / решил ее неправильно, создав тип? Если это не хорошее решение, что является лучшим решением?Как мое приложение должно обрабатывать случаи, когда
default(Airport)
используется? Тип неdefault(Airport)
имеет смысла для моего приложения, поэтому я занималсяif (airport == default(Airport) { throw ... }
там, где получение экземпляраAirport
(и егоCode
свойства) имеет решающее значение для операции.
Примечания: я рассмотрел вопросы C # / VB struct - как избежать случая с нулевыми значениями по умолчанию, который считается недействительным для данной структуры? , и используйте struct или нет, прежде чем задавать мой вопрос, однако я думаю, что мои вопросы достаточно разные, чтобы гарантировать свой собственный пост.
источник
default(Airport)
проблему - просто запретить экземпляры по умолчанию. Вы можете сделать это, написав конструктор без параметров и бросивInvalidOperationException
илиNotImplementedException
в нем.Ответы:
Обновление: я переписал свой ответ, чтобы устранить некоторые неверные предположения о структурах C #, а также ОП, сообщающий нам в комментариях об использовании интернированных строк.
Если вы можете контролировать данные, поступающие в вашу систему, используйте класс, который вы опубликовали в своем вопросе. Если кто-то бежит,
default(Airport)
он получитnull
значение обратно. Обязательно напишите свой приватныйEquals
метод, который будет возвращать false при сравнении нулевых объектов Airport, а затем разрешите имNullReferenceException
летать в других местах кода.Однако, если вы берете данные в систему из источников, которые вы не контролируете, вам не нужно прерывать весь поток. В этом случае структура идеально подходит для простого факта,
default(Airport)
который даст вам нечто иное, чемnull
указатель. Создайте очевидное значение для представления «нет значения» или «значение по умолчанию», чтобы у вас было что печатать на экране или в файле журнала (например, «---»). На самом деле, я бы просто держал вcode
секрете и не выставлялCode
собственность вообще - просто сосредоточился бы здесь на поведении.В худшем случае преобразование
default(Airport)
в строку выводит на печать"---"
и возвращает ложь по сравнению с другими действующими кодами аэропорта. Любой код аэропорта по умолчанию не соответствует ничему, включая другие коды аэропорта по умолчанию.Да, структуры должны быть значениями, выделенными в стеке, и любые указатели на динамическую память в основном сводят на нет преимущества производительности структур, но в этом случае значение структуры по умолчанию имеет значение и обеспечивает некоторое дополнительное пулевое сопротивление для остальных элементов. заявление.
Из-за этого я бы немного изменил правила.
Оригинальный ответ (с некоторыми фактическими ошибками)
Если вы можете контролировать данные, поступающие в вашу систему, я бы поступил так, как предложил Роберт Харви в комментариях: создать конструктор без параметров и вызвать исключение при его вызове. Это предотвращает попадание недействительных данных в систему через
default(Airport)
.Однако, если вы берете данные в систему из источников, которые вы не контролируете, вам не нужно прерывать весь поток. В этом случае вы можете создать недействительный код аэропорта, но сделать его очевидной ошибкой. Для этого потребуется создать конструктор без параметров и установить что-
Code
то вроде «---»:Поскольку вы используете в
string
качестве кода, нет смысла использовать структуру. Структура распределяется в стеке только дляCode
размещения в качестве указателя на строку в динамической памяти, поэтому здесь нет разницы между классом и структурой.Если вы измените код аэропорта на массив из 3 элементов, то структура будет полностью размещена в стеке. Даже тогда объем данных не так велик, чтобы что-то изменить.
источник
Code
свойства, изменило бы это ваше обоснование относительно вашей точки нахождения строки в динамической памяти?Используйте шаблон Flyweight
Поскольку аэропорт, по сути, является неизменным, нет необходимости создавать более одного экземпляра какого-либо конкретного, скажем, SFO. Используйте Hashtable или аналогичный (заметьте, я парень из Java, а не C #, поэтому точные детали могут отличаться) для кэширования аэропортов при их создании. Перед созданием нового проверьте Hashtable. Вы никогда не освобождаете аэропорты, поэтому GC не нужно их освобождать.
Еще одно небольшое преимущество (по крайней мере, в Java, не уверенное в C #) заключается в том, что вам не нужно писать
equals()
метод, простое==
подойдет. То же самое дляhashcode()
.источник
getAirportOrCreate()
код правильно синхронизирован, нет никаких технических причин, по которым вы не можете создавать новые аэропорты по мере необходимости. Там могут быть деловые причины.Я не особо продвинутый программист, но разве это не идеальное применение для Enum?
Существуют разные способы создания перечислимых классов из списков или строк. Вот тот, который я видел в прошлом, но не уверен, что это лучший способ.
https://blog.kloud.com.au/2016/06/17/converting-webconfig-values-into-enum-or-list/
источник
Одна из причин, по которой вы наблюдаете больше активности GC, заключается в том, что вы сейчас создаете вторую строку -
.ToUpperInvariant()
версию исходной строки. Исходная строка имеет право на сборщик мусора сразу после запуска конструктора, а вторая - одновременно сAirport
объектом. Вы можете минимизировать его другим способом (обратите внимание на третий параметрstring.Equals()
):источник
GetHashCode
, должен только использоватьStringComparer.OrdinalIgnoreCase.GetHashCode(Code)
или подобный