스위치 / 패턴 매칭 아이디어


151

최근에 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 # 언어 지원 가능성에 대한 생각 / 입력 / 비판 등)을 환영합니다.


"만약 사람들이 위와 같은 구조로부터 많은 이점이 있다고 생각하는지 (언어 지원이없는 경우) 측정하려고합니까?" 이모, 네 비슷한 것이 존재하지 않습니까? 그렇지 않다면 가벼운 라이브러리를 작성하는 것이 좋습니다.
Konrad Rudolph

10
Select Case 문에서이를 지원하는 VB .NET을 사용할 수 있습니다. 에크!
Jim Burger

나는 또한 내 자신의 경적을 and 고 내 라이브러리에 링크를 추가합니다 : 기능-닷넷
Alexey Romanov

1
나는이 아이디어가 마음에 들며 스위치 케이스의 매우 훌륭하고 훨씬 유연한 형태를 만듭니다. 그러나 이것이 Linq와 같은 구문을 if-then 래퍼로 사용하는 실제적인 방법이 아닙니까? 나는 실제 거래, 즉 switch-case진술 대신에 이것을 사용하지 못하게 할 것 입니다. 나를 잘못 생각하지 마라. 제 위치에 있다고 생각하고 아마도 구현 방법을 찾을 것이다.
IAbstract

2
이 질문은 2 년이 넘었지만 C # 7이 패턴 매칭 기능으로 곧 나올 것이라고 언급하는 것이 적절합니다.
Abion47

답변:


22

나는 그것이 오래된 주제라는 것을 알고 있지만 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 #의 주목할만한 차이점은 패턴 일치의 완성입니다. 패턴 일치는 사용 가능한 경우 컴파일러에서 제공하는 가능한 모든 경우에 대해 설명합니다. 기본 사례가이를 수행한다고 합리적으로 주장 할 수 있지만 실제로는 런타임 예외이기도합니다.
VoronoiPotato '

37

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

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

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

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

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

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

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

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

다른 링크들 :


25

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

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

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

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

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


1
실제로-내가 가진 버전은 델리게이트 및 표현 버전 모두에서 단락을 일으 킵니다. 표현식 버전은 복합 조건부로 컴파일됩니다. 대리자 버전은 단순히 일련의 술어 및 기능 / 작업입니다. 일치하는 항목이 있으면 중지됩니다.
Marc Gravell

흥미 롭습니다-엉성한 표정에서 나는 그것이 메소드 체인처럼 보일 때 적어도 각 조건의 기본 점검을 수행해야한다고 가정했지만, 이제는 메소드가 실제로 객체 인스턴스를 연결하여 빌드하기 위해 메소드를 작성하고 있음을 알았습니다. 해당 진술을 제거하기 위해 답변을 편집하겠습니다.
Greg Beech

22

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

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

2
F # 버전에 감사드립니다. F #이 이것을 처리하는 방식이 마음에 드는 것 같지만, 지금은 (전체) F #이 올바른 선택인지 확신 할 수 없으므로 중간 지점을 걸어야합니다 ...
Marc Gravell

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

9

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

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

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

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


1
아마도. 사실, 나는 그것이 될 이유에 대한 확신 "살인자"인수 생각하는 투쟁 할 필요 ( "언어보다 복잡한 만드는 비용으로 몇 가장자리 경우에 아마 좋은"반대).
Marc Gravell

5

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


4

유형을 전환하는 것이 'C-sharpey'는 아니지만, 구성은 일반적인 용도에서 매우 도움이 될 것입니다. 나는 그것을 사용할 수있는 적어도 하나의 개인 프로젝트가 있습니다 (관리 가능한 ATM 임에도 불구하고). 식 트리를 다시 쓰면 컴파일 성능 문제가 많이 있습니까?


재사용을 위해 객체를 캐시하지 않는 경우 (컴파일러가 코드를 숨기는 것을 제외하고는 C # 람다 식의 작동 방식)가 아닙니다. 다시 작성하면 컴파일 된 성능이 확실히 향상되지만 정기적으로 사용하려면 (LINQ-to-Something 대신) 대리자 버전이 더 유용 할 것으로 기대합니다.
Marc Gravell

또한 반드시 스위치 온 타입 일 필요는 없으며 복합 조건부 (LINQ를 통해)로도 사용될 수 있지만 지저분한 x => Test? 결과 1 : (Test2? 결과 2 : (Test3? 결과 3 : 결과 4))
Marc Gravell

실제 컴파일 성능을 의미했지만 csc.exe에 걸리는 시간-C #에 익숙하지 않아서 실제로 문제가 있는지는 알지 못하지만 C ++의 큰 문제입니다.
Simon Buchan

csc는 깜빡이지 않을 것입니다. LINQ의 작동 방식과 매우 유사하며 C # 3.0 컴파일러는 LINQ / 확장 방법 등에서 매우 뛰어납니다.
Marc Gravell

3

나는 이것이 정말로 흥미로워 보인다고 생각하지만, 조심해야 할 한 가지 : C # 컴파일러는 스위치 문을 최적화하는 데 매우 능숙합니다. 단락뿐만 아니라 얼마나 많은 사례에 따라 완전히 다른 IL을 얻습니다.

귀하의 특정 예는 내가 매우 유용하다고 생각하는 것을 수행합니다-예를 들어 typeof(Motorcycle)상수가 아니기 때문에 유형별로 대소 문자가 구문 이 없습니다.

이는 동적 응용 프로그램에서 더욱 흥미로워집니다. 여기서 논리는 데이터 중심으로 쉽게 '규칙 엔진'스타일의 실행을 제공 할 수 있습니다.


0

내가 작성한 라이브러리 인 OneOf를 사용하여 원하는 것을 얻을 수 있습니다.

switch(및 ifand exceptions as control flow) 의 주요 장점 은 컴파일 타임에 안전하다는 것입니다. 기본 처리기가 없거나 넘어집니다

   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에 있으며 net451 및 netstandard1.6을 대상으로합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.