структура з безглуздим значенням за замовчуванням


12

У моїй системі я часто працюю з кодами аеропортів ( "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)використовується.

Мої запитання:

  1. Чи було створення Airportкласу чи структури хорошим рішенням взагалі, чи я вирішую неправильну задачу / вирішую її неправильним шляхом, створюючи тип? Якщо це не гарне рішення, яке краще рішення?

  2. Як моя програма повинна обробляти випадки, коли default(Airport)використовується? Тип default(Airport)мого додатку не є чутливим, тому я робив if (airport == default(Airport) { throw ... }місця, де отримання екземпляра Airport(та його Codeвластивості) є критично важливим для операції.

Примітки. Я переглянув питання C # / VB structure - як уникнути випадку з нульовими значеннями за замовчуванням, що вважається недійсним для даної структури? , і використовуйте структуру чи ні перед тим, як ставити запитання, проте я думаю, що мої запитання є досить різними, щоб гарантувати власну посаду.


7
Чи має збір сміття істотний вплив на ефективність вашої заявки? Іншими словами, чи це має значення?
Роберт Харві

У будь-якому випадку, так, рішення класу було "хорошим". Як ви знаєте, що це вирішило вашу проблему, не створюючи нових.
Роберт Харві

2
Один із способів вирішити default(Airport)проблему - просто заборонити екземпляри за замовчуванням. Ви можете зробити це, написавши конструктор без параметрів і метання InvalidOperationExceptionабо NotImplementedExceptionв ньому.
Роберт Харві

3
Зі сторони замість підтвердження того, що ваша ініціалізаційна рядок насправді є 3 альфа-символами, чому б просто не порівняти її з кінцевим списком усіх кодів аеропортів (наприклад, github.com/datasets/airport-codes чи подібними)?
Ден Пішельман

2
Я готовий зробити ставку на кілька сортів пива, що це не корінь проблеми продуктивності. Звичайний ноутбук може виділяти в порядку 10М об'єктів / секунд.
Есбен Сков Педерсен

Відповіді:


6

Оновлення: я переписав свою відповідь, щоб вирішити деякі невірні припущення щодо C # структур, а також ОП, що повідомляє нас у коментарях про те, що використовуються інтерновані рядки.


Якщо ви можете контролювати дані, що надходять до вашої системи, використовуйте клас, який ви розмістили у своєму запитанні. Якщо хтось біжить, default(Airport)він отримає nullзначення назад. Не забудьте написати свій приватний Equalsметод, щоб повернути помилкове, коли порівнюєте нульові об’єкти аеропорту, а потім дозвольте NullReferenceExceptionлетіти в інше місце в коді.

Однак якщо ви берете дані в систему з джерел, якими ви не керуєте, вам не потрібно переривати весь потік. У цьому випадку структура є ідеальною для простого факту default(Airport)дасть вам щось, крім nullвказівника. Складіть очевидне значення для відображення "немає значення" або "значення за замовчуванням", щоб у вас є що надрукувати на екрані або у файлі журналу (наприклад, "---"). Насправді я б просто зберегти codeприватне і взагалі не виставляти Codeмайно - просто зосередитись на поведінці.

public struct Airport
{
    private string code;

    public Airport(string code)
    {
        // Check `code` for validity, throw exceptions if not valid

        this.code = code;
    }

    public override string ToString()
    {
        return code ?? (code = "---");
    }

    // int GetHashcode()

    // bool Equals(...)

    // bool operator ==(...)

    // bool operator !=(...)

    private bool Equals(Airport other)
    {
        if (other == null)
            // Even if this method is private, guard against null pointers
            return false;

        if (ToString() == "---" || other.ToString() == "---")
            // "Default" values should never match anything, even themselves
            return false;

        // Do a case insensitive comparison to enforce logic that airport
        // codes are not case sensitive
        return string.Equals(
            ToString(),
            other.ToString(),
            StringComparison.InvariantCultureIgnoreCase);
    }
}

Гірший сценарій перетворення default(Airport)в рядок видає "---"і повертає помилкове порівняно з іншими дійсними кодами аеропортів. Будь-який "код за умовчанням" аеропорту не відповідає нічого, включаючи інші коди аеропортів за замовчуванням.

Так, структури мають на увазі значення, виділені на стек, і будь-які вказівки на накопичення пам'яті в основному заперечують переваги продуктивності конструкцій, але в цьому випадку значення за замовчуванням структури має значення і забезпечує деякий додатковий опір кулі для решти частини застосування.

Я би тут трохи схилив правила, через це.


Оригінальний відповідь (з деякими фактичними помилками)

Якщо ви можете керувати даними, які надходять до вашої системи, я б зробив так, як у коментарях запропонував Роберт Харві: Створіть конструктор без параметрів і киньте виняток, коли він викликається. Це запобігає потраплянню недійсних даних у систему через default(Airport).

public Airport()
{
    throw new InvalidOperationException("...");
}

Однак якщо ви берете дані в систему з джерел, якими ви не керуєте, вам не потрібно переривати весь потік. У цьому випадку ви можете створити код аеропорту, який недійсний, але зробити це очевидною помилкою. Це передбачає створення конструктора без параметрів і встановлення значення Codeна зразок "---":

public Airport()
{
    Code = "---";
}

Оскільки ви використовуєте stringкод як код, немає сенсу використовувати структуру. Структура виділяється на стек, лише щоб мати Codeвиділений як вказівник на рядок в купі пам'яті, тому тут немає різниці між класом і Stru.

Якщо ви змінили код аеропорту на масив з 3 елементами char, то структура буде повністю розподілена на стеку. Навіть тоді обсяг даних не такий великий, щоб змінити значення.


Якби моя програма використовувала інтернетні рядки для Codeвластивості, чи змінить це ваше обґрунтування щодо того, що ваша точка рядка знаходиться в купі пам'яті?
Матвій

@Matthew: Чи використання класу створює проблеми з продуктивністю? Якщо ні, то переверніть монету, щоб вирішити, яку використовувати.
Грег Бургхардт

4
@Matthew: Дійсно важливо, що ви централізували клопітку логіку нормалізації кодів та порівнянь. Після цього "клас проти структури" - це лише академічна дискусія, поки ви не виміряєте достатньо великий вплив на продуктивність, щоб виправдати додатковий час розробника для проведення академічної дискусії.
Грег Бургхардт

1
Це правда, я не заперечую час від часу проводити академічну дискусію, якщо це допоможе мені створити більш обізнані рішення у майбутньому.
Метью

@Matthew: Так, ви абсолютно праві. Кажуть, "розмова дешева". Це, звичайно, дешевше, ніж не говорити і будувати щось погано. :)
Грег Бургхардт

13

Використовуйте шаблон Flyweight

Оскільки аеропорт, як правило, непорушний, немає необхідності створювати більше ніж один екземпляр будь-якого конкретного, скажімо, SFO. Використовуйте Hashtable або подібне (зверніть увагу, я хлопець на Java, а не C #, тому точні деталі можуть відрізнятися), щоб кешувати аеропорти, коли вони будуть створені. Перш ніж створити новий, перевірте в Hashtable. Ви ніколи не звільняєте аеропорти, тому GC не має потреби їх звільняти.

Ще однією незначною перевагою (принаймні на Java, не впевнені в C #) є те, що вам не потрібно писати equals()метод, це ==буде просто . Те саме для hashcode().


3
Прекрасне використання легкої ваги.
Ніл

2
Якщо припустимо, що OP продовжує використовувати структуру, а не клас, чи не є інтернування інтерфейсу, що вже обробляє багаторазові значення рядка? Структури вже живуть у стеці, рядки вже є повторно використані, щоб уникнути повторюваних значень у пам'яті. Яку додаткову вигоду ви отримаєте від моделі легкої ваги?
Площа

Щось, на що слідкувати. Якщо аеропорт доданий або видалений, ви хочете створити так, щоб оновити цей статичний список, не перезавантажуючи додаток або перерозподіляючи його. Аеропорти не додаються і не видаляються часто, але власники бізнесу, як правило, трохи засмучуються, коли проста зміна стає такою складною. "Не можу я просто додати його десь ?! Чому нам потрібно запланувати перезапуск випуску / програми та доставляти незручності нашим клієнтам?" Але я теж думав спочатку використовувати якийсь статичний кеш.
Грег Бургхардт

@Flater Розумна точка. Я б сказав, що для молодших програмістів потрібно менше міркувань про стек проти купи. Плюс дивіться моє доповнення - не потрібно писати рівних ().
user949300

1
@Greg Burghardt Якщо getAirportOrCreate()код правильно синхронізований, немає жодної технічної причини, щоб ви не могли створити нові аеропорти за потребою під час виконання. Можуть бути і ділові причини.
user949300

3

Я не особливо просунутий програміст, але хіба це не було б ідеальним використанням для Enum?

Існують різні способи побудови перелічених класів зі списків або рядків. Ось я бачив раніше, але не впевнений, чи це найкращий спосіб.

https://blog.kloud.com.au/2016/06/17/converting-webconfig-values-into-enum-or-list/


2
Якщо потенційно є тисячі різних значень (як це відбувається у кодах аеропортів), перерахування просто не практично.
Бен Коттрелл

Так, але посилання, яке я опублікував, полягає в тому, як завантажувати рядки як перерахунки. Ось ще одне посилання для завантаження таблиці пошуку як перерахунок. Це може бути трохи роботи, але скористатися силою перерахунків. виключенняnotfound.net/…
Адам Б

1
Або список дійсних кодів може бути завантажений із бази даних чи файлу. Тоді код аеропорту просто перевіряється, щоб він був серед цього списку. Це те, що ти зазвичай робиш, коли більше не хочеш жорстко кодувати значення та / або список стає довго керованим.
Ніл

@BenCottrell - саме для цього коду та шаблонів T4.
RubberDuck

3

Однією з причин, за якою ви бачите більше активності в GC, є те, що зараз ви створюєте другий рядок - .ToUpperInvariant()версію початкового рядка. Оригінальний рядок є придатним для GC відразу після запуску конструктора, а другий - придатний одночасно з Airportоб'єктом. Можливо, ви зможете мінімізувати його по-іншому (зверніть увагу на третій параметр string.Equals()):

public sealed class Airport : IEquatable<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;
    }

    public string Code { get; }

    public override string ToString()
    {
        return Code; // TODO: Upper-case it here if you really need to for display.
    }

    public bool Equals(Airport other)
    {
        return string.Equals(Code, other?.Code, StringComparison.InvariantCultureIgnoreCase);
    }

    public override bool Equals(object obj)
    {
        return obj is Airport airport && Equals(airport);
    }

    public override int GetHashCode()
    {
        return Code.GetHashCode();
    }

    public static bool operator ==(Airport left, Airport right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(Airport left, Airport right)
    {
        return !Equals(left, right);
    }
}

Чи це не дає різних хеш-кодів для рівних аеропортів (але з різною літери)?
Герой Блукає

Так, я б так уявив. Дангіт.
Джессі К. Слікер

Це дуже хороший момент, ніколи про це не думав, я буду дивитись на внесення цих змін.
Матвій

1
Що стосується GetHashCode, слід просто використовувати StringComparer.OrdinalIgnoreCase.GetHashCode(Code)або подібне
Матвій
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.