[C#] 스위치 / 패턴 매칭 아이디어

최근에 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와 비슷한 InRange (…) 일치와 같이보다 구체적인 일치로 간단한 확장 (직접 또는 확장 방법을 통해)을 허용합니다. “사용법.

사람들이 위와 같은 구문의 이점이 있다고 생각하는지 (언어 지원이없는 경우) 측정하려고합니까?

또한 위의 3 가지 변형을 가지고 놀았습니다.

  • 평가를위한 Func <TSource, TValue> 버전-복합 삼항 조건문과 비교
  • Action <TSource> 버전-if / else if / else if / else if / else와 비교
  • Expression <Func <TSource, TValue >> 버전-첫 번째 버전이지만 임의의 LINQ 공급자가 사용할 수 있음

또한 Expression 기반 버전을 사용하면 Expression-tree를 다시 작성하여 반복적으로 호출하지 않고 모든 분기를 단일 복합 조건부 Expression으로 인라인 할 수 있습니다. 최근에 확인하지는 않았지만 초기 Entity Framework 빌드에서는 InvocationExpression을별로 좋아하지 않았기 때문에 이것이 필요하다는 것을 상기하는 것 같습니다. 또한 반복되는 델리게이트 호출을 피하기 때문에 LINQ-to-Objects를보다 효율적으로 사용할 수 있습니다. 테스트는 동등한 C #에 비해 동일한 속도 (실제로 더 빠른 속도)로 수행하는 위와 같은 식 (표현식 사용)을 보여줍니다. 복합 조건문. 완벽을 기하기 위해 Func <…> 기반 버전은 C # 조건문보다 4 배나 걸렸지 만 여전히 매우 빠르며 대부분의 사용 사례에서 큰 병목이되지는 않습니다.

위의 (또는 풍부한 C # 언어 지원 가능성에 대한 생각 / 입력 / 비판 등)을 환영합니다.



답변

나는 그것이 오래된 주제라는 것을 알고 있지만 C # 7에서는 할 수 있습니다 :

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


답변

C #에서 이러한 “기능적”작업을 시도한 후에 (그리고 그에 대한 책을 시도한 후에), 몇 가지 예외를 제외하고는 그러한 일이 너무 도움이되지 않는다는 결론에 도달했습니다.

주된 이유는 F #과 같은 언어가 이러한 기능을 지원함으로써 많은 힘을 얻었 기 때문입니다. “당신이 할 수있는”것이 아니라 “단순하다, 분명하다, 예상된다”.

예를 들어, 패턴 일치에서 불완전한 일치가 있는지 또는 다른 일치가 적중하지 않는지를 컴파일러에게 알려줍니다. 이것은 개방형 유형에서는 유용하지 않지만 차별적 인 노동 조합 또는 튜플을 일치시킬 때 매우 유용합니다. F #에서는 사람들이 패턴 일치를 기대하고 즉시 의미가 있습니다.

“문제”는 기능적 개념을 사용하기 시작한 후에는 계속하는 것이 당연하다는 것입니다. 그러나 C #에서 튜플, 함수, 부분 메소드 적용 및 카레 링, 패턴 일치, 중첩 함수, 제네릭, 모나드 지원 등을 활용하면 매우 추악하고 매우 빠릅니다. 재미 있고 똑똑한 사람들이 C #에서 아주 멋진 일을했지만 실제로 사용 하는 것은 무겁습니다.

C #에서 (프로젝트 간) 자주 사용하는 결과 :

  • IEnumerable을위한 확장 메소드를 통한 시퀀스 함수. C # 구문이 잘 지원하기 때문에 ForEach 또는 Process ( “Apply”?-열거 된 순서 항목에 대한 작업 수행)와 같은 것이 적합합니다.
  • 일반적인 진술 패턴을 추상화합니다. 복잡한 try / catch / finally 블록 또는 기타 관련 (종종 매우 일반적인) 코드 블록. LINQ-to-SQL 확장도 여기에 적합합니다.
  • 튜플.

** 그러나 참고 : 자동 일반화 및 형식 유추가 없으면 이러한 기능을 사용하는 데 방해가됩니다. **

다른 누군가가 언급했듯이 소규모 팀에서 특정 목적을 위해 C #에 갇혀 있으면 도움이 될 수 있습니다. 그러나 내 경험상 그들은 일반적으로 YMMV보다 더 번거 로움을 느꼈습니다.

다른 링크들 :


답변

C #이 유형을 간단하게 전환하지 못하는 이유는 주로 객체 지향 언어이기 때문에 객체 지향 용어로이를 수행하는 ‘올바른’방법은 Vehicle에 GetRentPrice 메소드를 정의하고 파생 클래스에서 재정의합니다.

즉, 이러한 유형의 기능을 가진 F # 및 Haskell과 같은 다중 패러다임 및 기능 언어를 사용하여 약간의 시간을 보냈습니다. 스위치를 켜는 데 필요한 유형을 작성하지 않으므로 가상 메소드를 구현할 수 없습니다.) 이는 차별 된 노조와 함께 언어에 환영하는 것입니다.

[편집 : Marc가 단락 될 수 있다고 표시 한 것처럼 성능에 관한 부분을 제거함]

또 다른 잠재적 인 문제는 유용성 문제입니다. 마지막 호출에서 일치하는 조건이 충족되지 않으면 어떤 일이 발생하지만 두 가지 이상의 조건과 일치하면 동작은 무엇입니까? 예외를 던져야합니까? 첫 번째 또는 마지막 경기를 반환해야합니까?

이런 종류의 문제를 해결하는 데 사용하는 방법은 유형을 키로 사용하고 람다를 값으로 사용하여 사전 필드를 사용하는 것입니다. 이는 객체 초기화 구문을 사용하여 생성하는 것이 매우 간결합니다. 그러나 이는 콘크리트 유형만을 설명하며 추가 술어를 허용하지 않으므로보다 복잡한 경우에는 적합하지 않을 수 있습니다. [Side note-C # 컴파일러의 출력을 보면 switch 문을 사전 기반 점프 테이블로 자주 변환하므로 유형 전환을 지원할 수없는 적절한 이유는 없습니다.]


답변

언어 확장처럼 작동하는 이러한 종류의 라이브러리는 널리 받아 들여질 것 같지는 않지만 재미있게 플레이 할 수 있으며 이것이 유용한 특정 도메인에서 작업하는 소규모 팀에게 실제로 유용 할 수 있습니다. 예를 들어, 이와 같은 임의의 유형 테스트를 수행하는 수많은 ‘비즈니스 규칙 / 논리’를 작성하는 경우 어떻게 유용한 지 알 수 있습니다.

이것이 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


답변

귀하의 질문에 대답하기 위해, 패턴 매칭 구문 구조가 유용하다고 생각합니다. 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));
        }
    }


답변

패턴 일치 ( 여기 에서 설명 됨)의 목적은 유형 사양에 따라 값을 분해하는 것입니다. 그러나 C #의 클래스 (또는 유형) 개념은 귀하에게 동의하지 않습니다.

다중 패러다임 언어 디자인에는 잘못된 점이 있습니다. 반대로 C #에 람다가 있으면 매우 좋으며 Haskell은 IO와 같은 명령을 수행 할 수 있습니다. 그러나 Haskell 방식이 아닌 매우 우아한 솔루션은 아닙니다.

그러나 순차적 절차 프로그래밍 언어는 람다 미적분학의 관점에서 이해 될 수 있고 C #은 순차적 절차 언어의 매개 변수 내에 잘 들어 맞기 때문에 적합합니다. 그러나 Haskell의 순수한 기능적 맥락에서 무언가를 취한 다음 그 기능을 순수하지 않은 언어에 넣으면 더 나은 결과를 보장 할 수 없습니다.

요점은 이것이 패턴 매칭 틱을 언어 디자인과 데이터 모델에 연결시키는 것입니다. 그러나 패턴 매칭은 일반적인 C # 문제를 해결하지 못하고 명령형 프로그래밍 패러다임에 잘 맞지 않기 때문에 C #의 유용한 기능이라고 생각하지 않습니다.


답변

그러한 일을하는 OO 방법을 IMHO는 방문자 패턴입니다. 방문자 멤버 메소드는 단순히 케이스 구조의 역할을하며 유형 자체를 들여다 볼 필요없이 언어 자체가 적절한 디스패치를 ​​처리하게합니다.