saçma sapan varsayılan değeri olan yapı


12

Benim sistemde sık sık havaalanı kodları (çalışabilir "YYZ", "LAX", "SFO"vs.), bunlar (büyük harf olarak temsil 3 harfi) aynı formatta her zaman vardır. Sistem tipik olarak API talebi başına bu (farklı) kodların 25-50'sini ele alır, toplamda binden fazla tahsis ile, bunlar uygulamamızın birçok katmanından geçirilir ve eşitlik açısından sık sık karşılaştırılır.

Sadece biraz iyi çalıştı etrafında dizeleri geçen ile başladı ama hızlı bir şekilde 3 haneli kod beklenen bir yerde yanlış bir kod geçerek programlama hataları çok fark ettim. Ayrıca, büyük / küçük harfe duyarlı olmayan bir karşılaştırma yapmamız gereken sorunlarla karşılaştık ve bunun yerine hatalarla sonuçlandık.

Bundan sonra, dizeleri geçmeyi bırakmaya Airportve havaalanı kodunu alan ve doğrulayan tek bir kurucuya sahip bir sınıf oluşturmaya karar verdim .

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);
    }
}

Bu, kodumuzu anlamayı çok daha kolay hale getirdi ve eşitlik kontrollerimizi, sözlük / set kullanımlarımızı basitleştirdik. Artık yöntemlerimiz Airport, beklediğimiz gibi davranacağını kabul eden bir örneği kabul ederse , yöntem denetimlerimizi bir boş başvuru denetimine basitleştirdiğini biliyoruz.

Bununla birlikte, fark ettiğim şey, çöp toplama işleminin daha sık çalıştığıydı, bu Airportda toplanmanın birçok örneğini izledim .

Bu Benim çözüm dönüştürmek için oldu classbir içine struct. Çoğunlukla bu sadece bir anahtar kelime değişikliği hariç olmak üzere, oldu GetHashCodeve ToString:

public override string ToString()
{
    return Code ?? string.Empty;
}

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

Nerede default(Airport)kullanılır durumda ele .

Sorularım:

  1. Bir Airportsınıf ya da yapı oluşturmak genel olarak iyi bir çözüm müydü , yoksa yanlış problemi mi çözüyorum / türü yaratarak yanlış şekilde mi çözüyorum? İyi bir çözüm değilse, daha iyi bir çözüm nedir?

  2. Uygulamamın kullanıldığı örnekleri nasıl ele alması gerekir default(Airport)? Bir tür default(Airport)benim uygulama için saçma, bu yüzden if (airport == default(Airport) { throw ... }bir Airport(ve onun Codeözelliği) bir örnek almak operasyon için kritik yerlerde yapıyorum .

Notlar: C # / VB yapılı sorular - gözden geçirilmiş yapı için geçersiz sayılan sıfır varsayılan değerlerden nasıl kaçınılır? ve sorumu sormadan önce yapısını kullan ya da kullanma , ancak sorularımın kendi gönderisini garanti edecek kadar farklı olduğunu düşünüyorum.


7
Çöp toplama işleminin uygulamanızın performansı üzerinde önemli bir etkisi var mı? Başka bir deyişle, önemli mi?
Robert Harvey

Her neyse, evet, sınıf çözümü "iyi" bir çözümdü. Bildiğiniz gibi, sorununuzu yenileri oluşturmadan çözdü.
Robert Harvey

2
default(Airport)Sorunu çözmenin bir yolu , varsayılan örneklere izin vermemektir. Bunu parametresiz bir kurucu yazarak InvalidOperationExceptionveya NotImplementedExceptioniçine atarak yapabilirsiniz .
Robert Harvey

3
Yan notta, başlatma dizenizin aslında 3 alfa karakter olduğunu doğrulamak yerine, neden sadece tüm havaalanı kodlarının sonlu listesiyle karşılaştırmıyorsunuz (örn. Github.com/datasets/airport-codes veya benzeri)?
Dan Pichelman

2
Birkaç biraya bunun bir performans sorununun kökü olmadığını iddia etmeye hazırım. Normal bir dizüstü bilgisayar, 10M / saniye hızında sipariş verebilir.
Esben Skov Pedersen

Yanıtlar:


6

Güncelleme: Cevabımı, C # yapıları ve OP'nin stajyer dizelerin kullanıldığını yorumlarda bize bildiren bazı yanlış varsayımları ele almak için yeniden yazdım.


Sisteminize gelen verileri kontrol edebiliyorsanız, sorunuzda belirttiğiniz gibi bir sınıf kullanın. Birisi koşarsa default(Airport)bir nulldeğer geri alır. EqualsNull Airport nesnelerini karşılaştırırken false değerini döndürmek için özel yönteminizi yazdığınızdan emin olun ve sonra NullReferenceExceptionkodun başka bir yerinde uçmasına izin verin .

Ancak, kontrol etmediğiniz kaynaklardan sisteme veri alıyorsanız, tüm iş parçacığının çökmesine gerek yoktur. Bu durumda, basit bir gerçek default(Airport)size bir nullişaretçi dışında bir şey verecek bir yapı idealdir . Ekranda veya bir günlük dosyasında (örneğin "---" gibi) yazdırılacak bir şey olması için "değer yok" veya "varsayılan değer" i temsil etmek için bariz bir değer oluşturun. Aslında, ben sadece codeözel tutmak ve bir Codeözellik hiç maruz değil - sadece burada davranış odaklanmak.

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)Bir dizeye dönüştürülen daha kötü durum senaryosu yazdırılır "---"ve diğer geçerli Havaalanı kodlarıyla karşılaştırıldığında false değerini döndürür. Herhangi bir "varsayılan" havaalanı kodu, diğer varsayılan havaalanı kodları dahil hiçbir şeyle eşleşmez.

Evet, yapıların yığına tahsis edilen değerler olması amaçlanmıştır ve hafızayı biriktirmek için herhangi bir işaretçi temel olarak yapıların performans avantajlarını olumsuz yönde etkilemektedir, ancak bu durumda bir yapının varsayılan değeri bir anlam ifade eder ve uygulama.

Bu yüzden kuralları burada biraz bükerdim.


Orijinal Yanıt (bazı gerçek hatalarla)

Sisteminize gelen verileri kontrol edebiliyorsanız, Robert Harvey'nin yorumlarda önerdiği gibi yapardım: Parametresiz bir kurucu oluşturun ve çağrıldığında bir istisna atın. Bu, geçersiz verilerin sisteme üzerinden girmesini önler default(Airport).

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

Ancak, kontrol etmediğiniz kaynaklardan sisteme veri alıyorsanız, tüm iş parçacığının çökmesine gerek yoktur. Bu durumda, geçersiz bir havaalanı kodu oluşturabilir, ancak bunu bariz bir hata gibi gösterebilirsiniz. Bu, parametresiz bir kurucu oluşturmayı ve Code"---" gibi bir şeye ayarlamayı içerir :

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

Bir kullanırken yana stringKanunu gibi bir yapı kullanılarak hiçbir nokta yoktur. Yapı yığın üzerinde tahsis edilir, sadece Codeyığın bellekte bir dizeye bir işaretçi olarak tahsis edilir, bu yüzden burada sınıf ve yapı arasında fark yoktur.

Havaalanı kodunu 3'lü karakter dizisiyle değiştirdiyseniz, yığına tamamen bir yapı tahsis edilir. O zaman bile, veri hacmi bir fark yaratmak için o kadar büyük değil.


Uygulamam Codeözellik için interned dizeleri kullanıyor olsaydı, bu dizenin yığın bellekte olmasıyla ilgili gerekçenizi değiştirir mi?
Matthew

@Matthew: Sınıf kullanmak size performans sorunu mu veriyor? Değilse, hangisini kullanacağınıza karar vermek için bir bozuk para çevirin.
Greg Burghardt

4
@Matthew: Gerçekten önemli olan, kodları ve karşılaştırmaları normalleştirmenin zahmetli mantığını merkezileştirmeniz. Bundan sonra "yapıya karşı sınıf" sadece akademik bir tartışmadır.
Greg Burghardt

1
Bu, gelecekte daha iyi bilgilendirilmiş çözümler üretmeme yardımcı olursa, zaman zaman akademik bir tartışma yapmayı umursamıyorum.
Matthew

@ Mathew: Evet, kesinlikle haklısın. "Konuşma ucuz" diyorlar. Kesinlikle konuşmak ve kötü bir şey inşa etmekten daha ucuzdur. :)
Greg Burghardt

13

Kullan Flyweight desen

Havaalanı doğru bir şekilde değişmez olduğu için SFO gibi herhangi bir tanesinin birden fazla örneğini oluşturmaya gerek yoktur. Havalimanları oluşturulduklarında önbelleğe almak için Hashtable veya benzeri (not, C # değil bir Java adamıyım, bu yüzden kesin ayrıntılar değişebilir) kullanın. Yeni bir tane oluşturmadan önce Hashtable'a bakın. Havalimanlarını asla serbest bırakmazsınız, bu yüzden GC'nin onları serbest bırakmasına gerek yoktur.

Ek bir küçük avantaj (en azından Java'da, C # hakkında emin değilim) bir equals()yöntem yazmanız gerekmediği , basit bir işlem ==olacaktır. Aynı hashcode().


3
Flyweight deseninin mükemmel kullanımı.
Neil

2
OP'nin bir sınıfı değil bir yapıyı kullanmaya devam ettiğini varsayarsak, dize stajyeni yeniden kullanılabilir dize değerlerini zaten işlemiyor mu? Yapılar zaten yığın üzerinde yaşıyor, dizeler bellekte yinelenen değerlerden kaçınmak için zaten yeniden kullanılıyor. Sinek siklet paterninden ne gibi yararlar elde edilebilir?
flater

Dikkat edilmesi gereken bir şey. Bir havaalanı eklenir veya kaldırılırsa, uygulamayı yeniden başlatmadan veya yeniden dağıtmadan bu statik listeyi yenileyecek şekilde oluşturmak istersiniz. Havaalanları sık sık eklenmez veya kaldırılmaz, ancak basit bir değişiklik bu karmaşık hale geldiğinde işletme sahipleri biraz üzülür. "Sadece bir yere ekleyemez miyim ?! Neden bir sürüm / uygulama yeniden başlatma ve müşterilerimize rahatsızlık vermeyi planlamak zorundayız?" Ama aynı zamanda ilk başta bir çeşit statik önbellek kullanmayı da düşünüyordum.
Greg Burghardt

@Dayanır Makul nokta. Genç programcıların yığın ve yığın hakkında akıl yürütmelerine daha az ihtiyaç olduğunu söyleyebilirim. Artı benim ek görmek - eşit () yazmaya gerek yok.
user949300

1
@Greg Burghardt getAirportOrCreate()Kod düzgün şekilde senkronize edilirse, çalışma zamanı sırasında gerektiğinde yeni Havaalanları oluşturamamanın teknik bir nedeni yoktur . Ticari nedenler olabilir.
user949300

3

Ben özellikle ileri bir programcı değilim, ama bu bir Enum için mükemmel bir kullanım olmaz mı?

Listelerden veya dizelerden enum sınıfları oluşturmanın farklı yolları vardır. İşte geçmişte gördüğüm, en iyi yol olup olmadığından emin değilim.

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


2
Potansiyel olarak binlerce farklı değer olduğunda (havaalanı kodlarında olduğu gibi), bir numaralandırma pratik değildir.
Ben Cottrell

Evet, ancak gönderdiğim bağlantı, dizeleri numaralandırma olarak nasıl yükleyeceğidir. Arama tablosunu numaralandırma olarak yüklemek için başka bir bağlantı. Biraz iş olabilir, ancak numaralandırmaların gücünden faydalanır. exceptionnotfound.net/…
Adam B

1
Veya geçerli kodların bir listesi bir veritabanından veya dosyadan yüklenebilir. Daha sonra bu listenin arasında bir havaalanı kodu kontrol edilir. Değerleri artık kodlamak istemediğinizde ve / veya listenin yönetilmesi uzun sürdüğünde, genellikle bunu yaparsınız.
Neil

@BenCottrell tam olarak kod gen ve T4 şablonları içindir.
RubberDuck

3

Daha fazla GC etkinliği görmenizin nedenlerinden biri, şimdi ikinci bir dize .ToUpperInvariant()(orijinal dizenin sürümü) oluşturmanızdır. Orijinal dize, kurucu çalıştıktan hemen sonra GC için uygundur ve ikincisi, Airportnesne ile aynı anda kullanılabilir. Farklı bir şekilde simge durumuna küçültebilirsiniz (üçüncü parametreye dikkat edin 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);
    }
}

Bu, eşit (ancak farklı şekilde büyük harfle yazılmış) Havaalanlar için farklı karma kodlar vermez mi?
Hero Wanders

Evet, öyle düşünürdüm. Dangit.
Jesse C. Slicer

Bu çok iyi bir nokta, hiç düşünmedim, bu değişiklikleri yapmaya bakacağım.
Matthew

1
İle ilgili olarak GetHashCode, sadece kullanmalı StringComparer.OrdinalIgnoreCase.GetHashCode(Code)veya benzer
Matthew
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.