anahtar / kalıp eşleme fikri


151

Son zamanlarda F # bakıyordum ve çitin yakın zamanda sıçraması muhtemel olmasa da, kesinlikle C # (veya kütüphane desteği) hayatı kolaylaştırabilecek bazı alanları vurgular.

Özellikle, çok zengin bir sözdizimi sağlayan geçerli eşleme / koşullu C # eşdeğerlerinden çok daha etkileyici olan F # desen eşleştirme yeteneğini düşünüyorum. Doğrudan bir örnek vermeye çalışmayacağım (benim F # buna bağlı değil), ancak kısaca şunları sağlar:

  • türe göre eşleştirme (ayrımcı sendikalar için tam kapsamlı denetimle) [bunun ayrıca ilişkili değişkenin türünü de ihlal ettiğini, üye erişimi vb. verdiğini unutmayın]
  • yüklemle eşleştir
  • yukarıdakilerin kombinasyonları (ve muhtemelen farkında olmadığım bazı senaryolar)

C # için bu zenginliğin sonunda [ahem] ödünç alması hoş olsa da, bu arada çalışma zamanında neler yapılabileceğine bakıyordum - örneğin, izin vermek için bazı nesneleri bir araya getirmek oldukça kolaydır:

var getRentPrice = new Switch<Vehicle, int>()
        .Case<Motorcycle>(bike => 100 + bike.Cylinders * 10) // "bike" here is typed as Motorcycle
        .Case<Bicycle>(30) // returns a constant
        .Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20)
        .Case<Car>(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20)
        .ElseThrow(); // or could use a Default(...) terminator

burada getRentPrice bir Func <Vehicle, int>.

[not - belki Switch / Case burada yanlış terimler ... ama fikri gösteriyor]

Bana göre, bu, tekrarlanan if / else kullanarak eşdeğerden çok daha net veya kompozit üçlü koşullu (önemsiz olmayan ifadeler için çok dağınık hale geliyor - parantez bolca). Ayrıca çok fazla dökümden kaçınır ve daha spesifik eşleşmelere basit bir şekilde uzatma (doğrudan veya uzatma yöntemleri ile), örneğin VB Seçimi ile karşılaştırılabilir bir InRange (...) eşleşmesi sağlar ... Durum "x To y "kullanım.

İnsanların yukarıdaki gibi yapılardan çok fazla fayda sağladığını düşünüyorsa (dil desteğinin yokluğunda) ölçmeye çalışıyorum?

Ayrıca yukarıdakilerin 3 çeşidi ile oynadığımı unutmayın:

  • a Func <TSource, TValue> sürümü değerlendirme - kompozit üçlü koşullu ifadelerle karşılaştırılabilir
  • Eylem <TSource> sürümü - if / else if / else if / else if / else ile karşılaştırılabilir
  • bir İfade <İşlev <TSource, TValue >> sürümü - ilk olarak, ancak keyfi LINQ sağlayıcıları tarafından kullanılabilir

Ayrıca, İfade tabanlı sürümün kullanılması, tekrarlanan çağırma yerine temelde tüm dalları tek bir bileşik koşullu İfade haline getirerek İfade ağacının yeniden yazılmasını sağlar. Yakın zamanda kontrol etmedim, ancak bazı erken Entity Framework yapılarında, InvocationExpression'ı çok sevmediği için gerekli olduğunu hatırlıyorum. Ayrıca, tekrarlanan delege çağrılarını önlediği için LINQ'dan Nesnelere daha verimli kullanım sağlar - testler, eşdeğer C # ile karşılaştırıldığında aynı hızda (aslında marjinal olarak daha hızlı) performans gösteren yukarıdaki gibi (İfade formunu kullanarak) bir eşleşme gösterir bileşik koşullu deyim. Tamlık için, Func <...> tabanlı sürüm, C # koşullu ifadesinin 4 katı kadar sürdü, ancak yine de çok hızlı ve çoğu kullanım durumunda büyük bir darboğaz olması pek olası değil.

Yukarıdakiler (ya da daha zengin C # dil desteği olanakları üzerine düşüncelerinizi / giriş / eleştirilerinizi / vb.


"Sadece insanlar yukarıdaki gibi yapılardan çok fazla fayda olduğunu düşünüyorsa (dil desteği olmadan) ölçmeye çalışıyorum?" IMHO, evet. Benzer bir şey zaten yok mu? Değilse, hafif bir kütüphane yazmaya teşvik edin.
Konrad Rudolph

10
Select case deyiminde bunu destekleyen VB .NET'i kullanabilirsiniz. Eek!
Jim Burger

Ayrıca kendi boynuzumu da alacağım ve kütüphaneme bir bağlantı ekleyeceğim: fonksiyonel-dotnet
Alexey Romanov

1
Bu fikri seviyorum ve çok güzel ve çok daha esnek bir anahtarlama durumu oluşturuyor; Ancak, bu gerçekten bir o zaman sarmalayıcı olarak Linq benzeri sözdizimini kullanmanın süslenmiş bir yolu değil mi? Birinin bunu gerçek anlaşma, yani bir switch-caseifade yerine kullanmasını caydırırdım . Beni yanlış anlamayın, bence yeri var ve muhtemelen bir uygulama yolu arayacağım.
IAbstract

2
Bu soru iki yaşın üzerinde olmasına rağmen, C # 7'nin desen eşleştirme yetenekleriyle yakında (ish) çıkacağından bahsetmek uygun görünüyor.
Abion47

Yanıtlar:


22

Eski bir konu olduğunu biliyorum, ancak c # 7'de şunları yapabilirsiniz:

switch(shape)
{
    case Circle c:
        WriteLine($"circle with radius {c.Radius}");
        break;
    case Rectangle s when (s.Length == s.Height):
        WriteLine($"{s.Length} x {s.Height} square");
        break;
    case Rectangle r:
        WriteLine($"{r.Length} x {r.Height} rectangle");
        break;
    default:
        WriteLine("<unknown shape>");
        break;
    case null:
        throw new ArgumentNullException(nameof(shape));
}

Burada C # ve F # arasındaki önemli fark, desen eşleşmesinin bütünlüğüdür. Örüntü eşleşmesinin, derlemeden edinmediğiniz takdirde, tümüyle açıklanmış olası tüm durumları kapsar. Varsayılan durumun bunu yaptığını haklı olarak iddia edebilirsiniz, ancak aynı zamanda pratikte bir çalışma zamanı istisnasıdır.
VoronoiPotato

37

C # 'da böyle "işlevsel" şeyler yapmaya çalıştıktan sonra (ve hatta bir kitap girişiminde bulunduktan sonra), hayır, birkaç istisna dışında, bu tür şeylerin çok fazla yardımcı olmadığı sonucuna vardım.

Bunun ana nedeni, F # gibi dillerin güçlerinin çoğunu bu özellikleri gerçekten desteklemesinden almasıdır. "Yapabilirsin" değil, "basit, açık, bekleniyor".

Örneğin, kalıp eşleştirmede, derleyiciye eksik bir maç olup olmadığını veya başka bir maçın asla vurulmayacağını bildirirsiniz. Bu, açık uçlu türler için daha az yararlıdır, ancak ayrımcı bir birlik veya tupllerle eşleşirken çok şıktır. F # 'da, insanların desen eşleştirmesini beklersiniz ve anında mantıklı olur.

"Sorun", bazı fonksiyonel kavramları kullanmaya başladığınızda, devam etmek istemeniz doğaldır. Bununla birlikte, C # dizilerini, işlevler, kısmi bir yöntem uygulaması ve imalatı, desen eşleştirme, iç içe fonksiyonlar, jenerik, tek hücreli canlı desteği vb yararlanarak alır çok çok hızlı bir şekilde, çirkin. Eğlenceli ve bazı çok akıllı insanlar C #'da çok güzel şeyler yaptılar, ama aslında onu kullanmak ağır geliyor.

Ne C (sık sık projeler arasında) kullanarak sona erdi:

  • IEnumerable için uzatma yöntemleri ile dizi fonksiyonları. ForEach veya Process ("Apply"?) Gibi şeyler - numaralandırıldığı gibi bir dizi öğesi üzerinde bir eylem gerçekleştirir, çünkü C # sözdizimi bunu iyi destekler.
  • Ortak ifade kalıplarının soyutlanması. Karmaşık try / catch / nihayet bloklar veya diğer ilgili (genellikle çok genel) kod blokları. LINQ-SQL'i genişletmek de buraya sığar.
  • Tuples, bir dereceye kadar.

** Ancak unutmayın: Otomatik genelleme ve tür çıkarımı eksikliği, bu özelliklerin bile kullanılmasını gerçekten engeller. **

Bütün bunlar, başka birinin belirttiği gibi, küçük bir ekipte, belirli bir amaç için, evet, belki de C # ile sıkışıp kalırsanız yardımcı olabilirler. Ama deneyimlerime göre, genellikle değerlerinden daha fazla güçlük hissettiler - YMMV.

Diğer bazı bağlantılar:


25

Muhtemelen C # 'ın tipin açılmasını basitleştirmemesinin nedeni, öncelikle nesne yönelimli bir dil olması ve bunu nesne yönelimli terimlerle yapmanın' doğru 'yolunun Araç üzerinde bir GetRentPrice yöntemi tanımlamasıdır. türetilmiş sınıflarda geçersiz kılar.

Bununla birlikte, bu tür bir yeteneğe sahip olan F # ve Haskell gibi çok paradigma ve fonksiyonel dillerle oynamak için biraz zaman geçirdim ve daha önce yararlı olacağı birkaç yere rastladım (örneğin, açmanız gereken türleri yazmıyoruz, bu yüzden onlara sanal bir yöntem uygulayamazsınız) ve bu ayrımcı sendikalarla birlikte dile hoş geldiniz.

[Düzenle: Marc'ın kısa devre yapabileceğini belirttiği gibi performans hakkındaki bölüm kaldırıldı]

Başka bir potansiyel sorun kullanılabilirliktir - son çağrıdan, maç herhangi bir koşulu karşılayamazsa ne olur, ancak iki veya daha fazla koşulla eşleşirse davranış nedir? Bir istisna atmalı mı? İlk maçı mı yoksa son maçı mı iade etmeli?

Bu tür bir sorunu çözmek için kullanmaya eğilimli bir yol, anahtar olarak lambda ve değer olarak lambda ile bir sözlük alanı kullanmak, nesne başlatıcı sözdizimi kullanarak oluşturmak için oldukça kısa; ancak bu yalnızca somut türü açıklar ve ek tahminlere izin vermez, bu nedenle daha karmaşık durumlar için uygun olmayabilir. [Yan not - C # derleyicisinin çıktısına bakarsanız, sık sık anahtar ifadelerini sözlük tabanlı atlama tablolarına dönüştürür, bu nedenle türlerin açılmasını destekleyememesinin iyi bir nedeni yoktur]


1
Aslında - benim sürüm hem delege hem de ifade sürümlerinde kısa devre yapar. İfade versiyonu bir bileşik koşullu olarak derlenir; delege sürümü basitçe bir tahminler ve işlev / eylemler kümesidir - bir eşleşme olduğunda durur.
Marc Gravell

İlginç - bir cursory bakışta bir yöntem zinciri gibi görünüyordu her durumun en azından temel kontrol yapmak zorunda olacağını varsaydı, ama şimdi yöntemleri aslında bunu yapmak için bir nesne örneği zincirleme olduğunu fark. Bu ifadeyi kaldırmak için cevabımı düzenleyeceğim.
Greg Beech

22

Bu tür kitaplıkların (dil uzantıları gibi davranan) geniş bir kabul kazanacağını düşünmüyorum, ancak oynamaktan zevk alıyorlar ve bunun yararlı olduğu belirli alanlarda çalışan küçük ekipler için gerçekten yararlı olabilirler. Örneğin, bunun gibi keyfi tip testleri yapan tonlarca 'iş kuralları / mantığı' yazıyorsanız, bunun nasıl kullanışlı olacağını görebiliyorum.

Bu bir C # dil özelliği olması muhtemel ise hiçbir fikrim yok (şüpheli görünüyor, ama geleceği kim görebilir?).

Referans için, karşılık gelen F # yaklaşık olarak:

let getRentPrice (v : Vehicle) = 
    match v with
    | :? Motorcycle as bike -> 100 + bike.Cylinders * 10
    | :? Bicycle -> 30
    | :? Car as car when car.EngineType = Diesel -> 220 + car.Doors * 20
    | :? Car as car when car.EngineType = Gasoline -> 200 + car.Doors * 20
    | _ -> failwith "blah"

çizgileri boyunca bir sınıf hiyerarşisi tanımladığınızı varsayarsak

type Vehicle() = class end

type Motorcycle(cyl : int) = 
    inherit Vehicle()
    member this.Cylinders = cyl

type Bicycle() = inherit Vehicle()

type EngineType = Diesel | Gasoline

type Car(engType : EngineType, doors : int) = 
    inherit Vehicle()
    member this.EngineType = engType
    member this.Doors = doors

2
F # sürümü için teşekkürler. Sanırım F #'ın bunu ele alma şeklini seviyorum, ama şu anda (genel olarak) F #'ın doğru seçim olduğundan emin değilim, bu yüzden o orta yoldan yürümek zorundayım ...
Marc Gravell

13

Sorunuzu cevaplamak için evet, kalıp eşleştirme sözdizimsel yapılarının yararlı olduğunu düşünüyorum. Ben biri için C # sözdizimsel destek görmek istiyorum.

İşte benim tanımladığımla aynı sözdizimini (neredeyse) sağlayan bir sınıfı uygulamam

public class PatternMatcher<Output>
{
    List<Tuple<Predicate<Object>, Func<Object, Output>>> cases = new List<Tuple<Predicate<object>,Func<object,Output>>>();

    public PatternMatcher() { }        

    public PatternMatcher<Output> Case(Predicate<Object> condition, Func<Object, Output> function)
    {
        cases.Add(new Tuple<Predicate<Object>, Func<Object, Output>>(condition, function));
        return this;
    }

    public PatternMatcher<Output> Case<T>(Predicate<T> condition, Func<T, Output> function)
    {
        return Case(
            o => o is T && condition((T)o), 
            o => function((T)o));
    }

    public PatternMatcher<Output> Case<T>(Func<T, Output> function)
    {
        return Case(
            o => o is T, 
            o => function((T)o));
    }

    public PatternMatcher<Output> Case<T>(Predicate<T> condition, Output o)
    {
        return Case(condition, x => o);
    }

    public PatternMatcher<Output> Case<T>(Output o)
    {
        return Case<T>(x => o);
    }

    public PatternMatcher<Output> Default(Func<Object, Output> function)
    {
        return Case(o => true, function);
    }

    public PatternMatcher<Output> Default(Output o)
    {
        return Default(x => o);
    }

    public Output Match(Object o)
    {
        foreach (var tuple in cases)
            if (tuple.Item1(o))
                return tuple.Item2(o);
        throw new Exception("Failed to match");
    }
}

İşte bazı test kodu:

    public enum EngineType
    {
        Diesel,
        Gasoline
    }

    public class Bicycle
    {
        public int Cylinders;
    }

    public class Car
    {
        public EngineType EngineType;
        public int Doors;
    }

    public class MotorCycle
    {
        public int Cylinders;
    }

    public void Run()
    {
        var getRentPrice = new PatternMatcher<int>()
            .Case<MotorCycle>(bike => 100 + bike.Cylinders * 10) 
            .Case<Bicycle>(30) 
            .Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20)
            .Case<Car>(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20)
            .Default(0);

        var vehicles = new object[] {
            new Car { EngineType = EngineType.Diesel, Doors = 2 },
            new Car { EngineType = EngineType.Diesel, Doors = 4 },
            new Car { EngineType = EngineType.Gasoline, Doors = 3 },
            new Car { EngineType = EngineType.Gasoline, Doors = 5 },
            new Bicycle(),
            new MotorCycle { Cylinders = 2 },
            new MotorCycle { Cylinders = 3 },
        };

        foreach (var v in vehicles)
        {
            Console.WriteLine("Vehicle of type {0} costs {1} to rent", v.GetType(), getRentPrice.Match(v));
        }
    }

9

Örüntü eşleştirme ( burada açıklandığı gibi ) amacı, değerlerini tür özelliklerine göre yapılandırarak yapmaktır. Ancak, C # 'daki bir sınıf (veya tür) kavramı sizinle aynı fikirde değildir.

Çoklu paradigma dili tasarımında yanlışlık var, aksine C # 'da lambdas olması çok güzel ve Haskell örneğin IO için zorunlu şeyler yapabilir. Ama bu çok zarif bir çözüm değil, Haskell tarzında değil.

Ancak sıralı işlemsel programlama dilleri lambda hesabı açısından anlaşılabildiğinden ve C #, sıralı bir yordamsal dilin parametrelerine iyi uyduğu için, bu iyi bir uyumdur. Ancak, Haskell'in diyelim ki saf işlevsel bağlamından bir şey almak ve daha sonra bu özelliği saf olmayan bir dile koymak, sadece bunu yapmak, daha iyi bir sonuç garanti etmeyecektir.

Demek istediğim, desen eşleştirme işaretini yapan şey dil tasarımı ve veri modeline bağlı. Bunu söyledikten sonra, tipik C # sorunlarını çözmediği veya zorunlu programlama paradigmasına iyi uymadığı için desen eşleşmesinin C #'ın yararlı bir özelliği olduğuna inanmıyorum.


1
Olabilir. Gerçekten de, neden ihtiyaç duyulacağına inandırıcı bir "katil" argümanı düşünmek için mücadele edecektim ("birkaç kenarda belki de dili daha karmaşık hale getirme pahasına güzel" aksine).
Marc Gravell

5

IMO, bu tür şeyleri yapmanın OO yolu Ziyaretçi kalıbıdır. Ziyaretçi üye yöntemleriniz basitçe vaka yapıları gibi davranır ve dilin türlere "göz atmak" zorunda kalmadan uygun dağıtımı yapmasına izin verirsiniz.


4

Türü açmak çok 'C-sharpey' olmamasına rağmen, yapının genel kullanımda oldukça yararlı olacağını biliyorum - onu kullanabilen en az bir kişisel projem var (yönetilebilir ATM'si olmasına rağmen). İfade ağacının yeniden yazılmasıyla ilgili bir derleme performansı sorunu mu var?


Nesneyi yeniden kullanmak üzere önbelleğe alırsanız (derleyici kodu gizlemesi dışında, C # lambda ifadelerinin büyük ölçüde nasıl çalıştığıdır). Yeniden yazma kesinlikle derlenmiş performansı artırır - ancak, düzenli kullanım için (LINQ-to-Something yerine) delege sürümünün daha yararlı olabileceğini bekliyorum.
Marc Gravell

Ayrıca not edin - mutlaka tipte bir anahtar değildir - kompozit koşullu (hatta LINQ aracılığıyla) olarak da kullanılabilir - ancak dağınık x => Test? Sonuç1: (Test2? Sonuç2: (Test3? Sonuç 3: Sonuç4))
Marc Gravell

Her ne kadar gerçek derleme performansı anlamasına rağmen bilmek güzel : csc.exe ne kadar sürer - bu gerçekten bir sorun olup olmadığını bilmek için C # ile yeterince tanıdık değilim, ama C ++ için büyük bir sorun.
Simon Buchan

csc bu noktada yanıp sönmez - LINQ'nun çalışma şekline çok benzer ve C # 3.0 derleyicisi LINQ / uzatma yöntemleri vb. konusunda oldukça iyidir.
Marc Gravell

3

Bence bu gerçekten ilginç görünüyor (+1), ama dikkat edilmesi gereken bir şey: C # derleyicisi anahtar ifadeleri optimize oldukça iyi. Sadece kısa devre için değil - sahip olduğunuz vaka sayısına bağlı olarak tamamen farklı IL elde edersiniz.

Özel örneğiniz çok yararlı bulabileceğim bir şey yapıyor - (örneğin) typeof(Motorcycle)sabit olmadığından , duruma göre eşdeğer bir sözdizimi yoktur .

Bu, dinamik uygulamada daha ilginç hale gelir - buradaki mantığınız kolayca veriye dayalı olabilir ve 'kural motoru' tarzı yürütme sağlar.


0

Yazdıklarımı OneOf adında bir kütüphane kullanarak daha sonra bulunduğunuz yere ulaşabilirsiniz.

En büyük avantajı switch(ve ifve exceptions as control flow) derleme zamanı güvenli olmasıdır - varsayılan bir işleyici veya düşme yoktur

   OneOf<Motorcycle, Bicycle, Car> vehicle = ... //assign from one of those types
   var getRentPrice = vehicle
        .Match(
            bike => 100 + bike.Cylinders * 10, // "bike" here is typed as Motorcycle
            bike => 30, // returns a constant
            car => car.EngineType.Match(
                diesel => 220 + car.Doors * 20
                petrol => 200 + car.Doors * 20
            )
        );

Nuget'te ve net451 ve netstandard1.6'yı hedefliyor

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.