158

私は最近 F# に注目しており、すぐに垣根を越えることはありそうにありませんが、C# (またはライブラリのサポート) が生活を楽にするいくつかの分野を明確に示しています。

特に、F# のパターン マッチング機能について考えています。これにより、非常に豊富な構文が可能になり、現在のスイッチ/条件付き C# の等価物よりもはるかに表現力豊かになります。直接的な例を挙げようとはしませんが (私の F# はそれに対応していません)、要するに次のことが可能です。

  • 型による照合 (判別共用体のフル カバレッジ チェックを使用) [これは、バインドされた変数の型も推測し、メンバー アクセスなどを与えることに注意してください]
  • 述語による一致
  • 上記の組み合わせ (および、おそらく私が認識していない他のシナリオ)

最終的に C# がこの豊富な機能の一部を借用できれば素晴らしいと思いますが、その間、私は実行時に何ができるかを調べてきました。

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

getRentPrice は Func<Vehicle,int> です。

[注 - おそらく、ここでのスイッチ/ケースは間違った用語です...しかし、それはアイデアを示しています]

私にとって、これは、if/else を繰り返し使用する同等の方法や、複合三項条件 (自明ではない式では非常に面倒です - 括弧がたくさんあります) よりもはるかに明確です。また、多くのキャストを回避し、VB の Select...Case "x To y " 利用方法。

上記のような構成要素に大きなメリットがあると人々が考えているかどうかを判断しようとしているだけです (言語サポートがない場合)。

さらに、上記の 3 つのバリエーションで遊んでいることに注意してください。

  • 評価用の Func<TSource,TValue> バージョン - 複合三項条件文に相当
  • Action<TSource> バージョン - if/else if/else if/else if/else と同等
  • Expression<Func<TSource,TValue>> バージョン - 最初のバージョンですが、任意の LINQ プロバイダーで使用できます

さらに、式ベースのバージョンを使用すると、繰り返し呼び出しを使用するのではなく、本質的にすべてのブランチを単一の複合条件付き式にインライン化して、式ツリーの書き換えが可能になります。私は最近チェックしていませんが、初期の Entity Framework ビルドでは、InvocationExpression があまり好きではなかったため、これが必要だったことを思い出すようです。また、デリゲート呼び出しの繰り返しが回避されるため、LINQ-to-Objects をより効率的に使用できます。テストでは、同等の C# と比較して、同じ速度 [実際にはわずかに速い] で実行される (式フォームを使用した) 上記のような一致が示されます。複合条件ステートメント。完全を期すために、Func<...> ベースのバージョンは C# 条件ステートメントの 4 倍の時間がかかりましたが、それでも非常に高速であり、ほとんどのユース ケースで大きなボトルネックになる可能性は低いです。

上記について (または、より豊富な C# 言語サポートの可能性について... 期待しています ;-p) についての考え、意見、批評などを歓迎します。

4

10 に答える 10

37

C# でそのような「機能的な」ことを試みた後 (そしてそれについての本を試したことさえありました)、いくつかの例外を除いて、そのようなことはあまり役に立たないという結論に達しました。

主な理由は、F# などの言語が、これらの機能を真にサポートすることで多くの能力を発揮することです。「できる」ではなく、「簡単、明快、当たり前」。

たとえば、パターン マッチングでは、不完全な一致があるかどうか、または別の一致がヒットしない場合は、コンパイラに通知されます。これはオープン エンド型ではあまり役に立ちませんが、識別された共用体またはタプルを照合する場合は非常に便利です。F# では、人々がパターン マッチを行うことを期待しますが、それはすぐに理にかなっています。

「問題」は、機能的な概念を使い始めると、それを続けたいと思うのは自然なことです。ただし、タプル、関数、部分メソッドの適用とカリー化、パターン マッチング、ネストされた関数、ジェネリック、モナド サポートなどを C# で利用すると、非常に見苦しくなります。楽しいし、非常に頭のいい人が C# で非常にクールなことをやっていますが、実際に使用すると重く感じます。

私がC#で(プロジェクト間で)頻繁に使用することになったもの:

  • IEnumerable の拡張メソッドによるシーケンス関数。ForEach や Process ("Apply"? -- 列挙されたシーケンス項目に対してアクションを実行する) などは、C# 構文が適切にサポートしているため、適合します。
  • 一般的なステートメント パターンの抽象化。複雑な try/catch/finally ブロックまたはその他の関連する (多くの場合非常に一般的な) コード ブロック。LINQ-to-SQL の拡張もここに当てはまります。
  • タプル、ある程度。

** ただし、注意してください: 自動一般化と型推論の欠如は、これらの機能の使用を実際に妨げます。**

これはすべて、他の誰かが述べたように、小さなチームで、特定の目的のために、はい、おそらくC#で立ち往生している場合に役立つ可能性があります. しかし、私の経験では、彼らは通常、価値があるよりも面倒だと感じました - YMMV.

その他のリンク:

于 2008-10-12T11:38:24.843 に答える
25

おそらく、C#が型の切り替えを簡単にしない理由は、それが主にオブジェクト指向言語であり、オブジェクト指向の用語でこれを行う「正しい」方法は、VehicleとGetRentPriceメソッドを定義することです。派生クラスでオーバーライドします。

そうは言っても、私はこのタイプの機能を備えたF#やHaskellのようなマルチパラダイムや関数型言語で遊ぶことに少し時間を費やしました。スイッチをオンにする必要のある型を記述していないため、仮想メソッドを実装できません)。これは、識別された共用体とともに言語に歓迎されるものです。

[編集:マークが短絡する可能性があることを示したため、パフォーマンスに関する部分を削除しました]

もう1つの潜在的な問題は、ユーザビリティの問題です。最後の呼び出しから、一致がいずれかの条件を満たさない場合に何が起こるかは明らかですが、2つ以上の条件に一致する場合の動作はどうなりますか?例外をスローする必要がありますか?最初の一致または最後の一致を返す必要がありますか?

この種の問題を解決するために私がよく使用する方法は、タイプをキー、ラムダを値としてディクショナリフィールドを使用することです。これは、オブジェクト初期化構文を使用して構築するのは非常に簡単です。ただし、これは具体的なタイプのみを考慮し、追加の述語を許可しないため、より複雑なケースには適さない場合があります。[補足-C#コンパイラの出力を見ると、switchステートメントが辞書ベースのジャンプテーブルに変換されることが多いため、型の切り替えをサポートできなかった正当な理由はないようです]

于 2008-10-01T07:59:57.430 に答える
23

この種のライブラリ (言語拡張機能のように機能する) が広く受け入れられる可能性は低いと思いますが、楽しく遊べますし、これが役立つ特定のドメインで作業する小さなチームにとっては非常に便利です。たとえば、このような任意の型のテストを行う大量の「ビジネス ルール/ロジック」を作成している場合、それがいかに便利であるかがわかります。

これが C# 言語の機能である可能性が高いかどうかはわかりません (疑わしいようですが、誰が未来を見ることができますか?)。

参考までに、対応する F# はおよそ次のとおりです。

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"

の行に沿ってクラス階層を定義したと仮定します

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
于 2008-10-01T07:26:01.347 に答える
13

はい、パターン マッチングの構文構造は便利だと思います。私は、C# での構文サポートを見たいと思っています。

これは、あなたが説明したのと(ほぼ)同じ構文を提供するクラスの私の実装です

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

ここにいくつかのテストコードがあります:

    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));
        }
    }
于 2011-09-04T20:22:40.137 に答える
9

パターン マッチング (ここで説明) の目的は、型指定に従って値を分解することです。ただし、C# のクラス (または型) の概念はあなたの意見に同意しません。

マルチパラダイム言語設計に問題はありません。逆に、C# にラムダがあることは非常に優れており、Haskell は IO などに命令型の処理を実行できます。しかし、これは Haskell の流儀ではなく、非常にエレガントなソリューションではありません。

しかし、シーケンシャル プロシージャル プログラミング言語はラムダ計算の観点から理解することができ、C# はたまたまシーケンシャル プロシージャル言語のパラメーター内にうまく収まるため、うまく適合します。しかし、Haskell などの純粋な関数型コンテキストから何かを取り出して、その機能を純粋ではない言語に組み込むと、それだけでより良い結果が保証されるわけではありません。

私が言いたいのは、パターン マッチングを有効にするものは、言語設計とデータ モデルに結びついているということです。そうは言っても、パターン マッチングが C# の有用な機能であるとは思えません。パターン マッチングは C# の典型的な問題を解決しないし、命令型プログラミング パラダイムにもうまく適合しないからです。

于 2009-08-13T07:29:01.857 に答える
5

私の謙虚な意見では、そのようなことを行うオブジェクト指向の方法はビジター パターンです。ビジター メンバー メソッドは単にケース コンストラクトとして機能し、型を「覗く」必要なく、言語自体が適切なディスパッチを処理できるようにします。

于 2009-06-16T07:05:34.040 に答える
4

タイプをオンにするのはあまり「C-sharpey」ではありませんが、コンストラクトが一般的な使用で非常に役立つことはわかっています-それを使用できる個人プロジェクトが少なくとも1つあります(ただし、ATMは管理可能です)。式ツリーの書き直しにより、コンパイルのパフォーマンスに大きな問題がありますか?

于 2008-10-01T07:13:46.400 に答える
3

注意すべきことの 1 つは、C# コンパイラは、switch ステートメントの最適化に非常に優れていることです。短絡だけでなく、ケースの数などに応じて、まったく異なる IL が得られます。

あなたの特定の例は、私が非常に役立つと思うことを行います-(たとえば)typeof(Motorcycle)は定数ではないため、ケースバイタイプに相当する構文はありません。

これは、動的アプリケーションではさらに興味深いものになります。ここでのロジックは、「ルールエンジン」スタイルの実行を可能にする、簡単にデータ駆動型にすることができます。

于 2008-10-01T09:16:07.557 に答える