มีทางเลือกอื่นที่ดีกว่านี้ในการ "เปิดใช้ประเภท" หรือไม่


331

การเห็นว่า C # ไม่สามารถใช้switchกับประเภท (ซึ่งฉันรวบรวมไม่ได้เพิ่มเป็นกรณีพิเศษเนื่องจากisความสัมพันธ์หมายความว่าcaseอาจมีการใช้มากกว่าหนึ่งที่แตกต่างกัน) มีวิธีที่ดีกว่าในการจำลองการสลับกับชนิดอื่นนอกเหนือจากนี้หรือไม่

void Foo(object o)
{
    if (o is A)
    {
        ((A)o).Hop();
    }
    else if (o is B)
    {
        ((B)o).Skip();
    }
    else
    {
        throw new ArgumentException("Unexpected type: " + o.GetType());
    }
}

18
ทำไมคุณไม่ใช้ความแตกต่าง?

18
@jeyoung ผนึกคลาสแล้วและมันก็ไม่คุ้มค่าสำหรับสถานการณ์แบบเฉพาะกิจ
xyz



2
@jeyoung: สถานการณ์ทั่วไปหนึ่งที่ไม่สามารถใช้ polymorphism ได้คือเมื่อชนิดที่ถูกสลับไปต้องไม่ทราบรหัสที่มีswitchคำสั่ง ตัวอย่างหนึ่ง: แอสเซมบลีAประกอบด้วยชุดของวัตถุข้อมูล (ซึ่งจะไม่เปลี่ยนแปลงกำหนดไว้ในเอกสารข้อมูลจำเพาะหรือเช่น) แอสเซมบลีB , CและDแต่ละการอ้างอิงAและจัดให้มีการแปลงสำหรับวัตถุข้อมูลต่าง ๆ จากA (เช่นอนุกรม / deserialization เป็นรูปแบบเฉพาะบางอย่าง) คุณต้องสะท้อนลำดับชั้นของคลาสทั้งหมดในB , CและDและใช้โรงงานหรือคุณมี ...
หรือผู้ทำแผนที่

คำตอบ:


276

การสลับกับประเภทขาดใน C # ( อัปเดต: ใน C # 7 / VS 2017 รองรับการสลับประเภท - ดูคำตอบของ Zachary Yates ด้านล่าง ) ในการดำเนินการนี้โดยไม่มีคำสั่ง if / if if / else ขนาดใหญ่คุณจะต้องทำงานกับโครงสร้างอื่น ฉันเขียนโพสต์บล็อกชั่วครู่กลับรายละเอียดวิธีการสร้างโครงสร้าง TypeSwitch

https://docs.microsoft.com/archive/blogs/jaredpar/switching-on-types

เวอร์ชั่นย่อ: TypeSwitch ได้รับการออกแบบเพื่อป้องกันการส่งข้อมูลซ้ำซ้อนและให้ไวยากรณ์ที่คล้ายกับคำสั่ง switch / case ปกติ ตัวอย่างเช่นนี่คือ TypeSwitch ที่ทำงานบนเหตุการณ์แบบฟอร์ม Windows มาตรฐาน

TypeSwitch.Do(
    sender,
    TypeSwitch.Case<Button>(() => textBox1.Text = "Hit a Button"),
    TypeSwitch.Case<CheckBox>(x => textBox1.Text = "Checkbox is " + x.Checked),
    TypeSwitch.Default(() => textBox1.Text = "Not sure what is hovered over"));

รหัสสำหรับ TypeSwitch มีขนาดค่อนข้างเล็กและสามารถใส่ในโครงการของคุณได้อย่างง่ายดาย

static class TypeSwitch {
    public class CaseInfo {
        public bool IsDefault { get; set; }
        public Type Target { get; set; }
        public Action<object> Action { get; set; }
    }

    public static void Do(object source, params CaseInfo[] cases) {
        var type = source.GetType();
        foreach (var entry in cases) {
            if (entry.IsDefault || entry.Target.IsAssignableFrom(type)) {
                entry.Action(source);
                break;
            }
        }
    }

    public static CaseInfo Case<T>(Action action) {
        return new CaseInfo() {
            Action = x => action(),
            Target = typeof(T)
        };
    }

    public static CaseInfo Case<T>(Action<T> action) {
        return new CaseInfo() {
            Action = (x) => action((T)x),
            Target = typeof(T)
        };
    }

    public static CaseInfo Default(Action action) {
        return new CaseInfo() {
            Action = x => action(),
            IsDefault = true
        };
    }
}

26
"type == entry.Target" สามารถเปลี่ยนเป็น "entry.Target.IsAssignableFrom (type)" เพื่อใช้ประเภทที่เข้ากันได้ (เช่นคลาสย่อย) เข้าในบัญชี
Mark Cidade

แก้ไขรหัสเพื่อใช้ "entry.Target.IsAssignableFrom (type)" เพื่อให้คลาสย่อยได้รับการสนับสนุน
Matt Howells

3
สิ่งหนึ่งที่ควรค่าแก่การสังเกตคือ (จากสิ่งที่ฉันเข้าใจ) จะต้องระบุการกระทำ 'เริ่มต้น' ครั้งสุดท้ายเพื่อให้แน่ใจว่าทุกกรณีจะได้รับการตรวจสอบ ฉันเชื่อว่านี่ไม่ใช่ข้อกำหนดในสวิตช์มาตรฐาน - ไม่ใช่ว่าฉันเคยเห็นใครพยายามปลูก 'ค่าเริ่มต้น' ที่อื่นนอกเหนือจากด้านล่าง คู่ของล้มเหลวในตัวเลือกที่ปลอดภัยสำหรับการนี้อาจจะที่จะสั่งซื้ออาร์เรย์เพื่อให้แน่ใจว่าเริ่มต้นคือที่ผ่านมา (สิ้นเปลืองบิต) หรือป๊อปเริ่มต้นในตัวแปรที่ต้องดำเนินการหลังจากที่foreach(ซึ่งจะเท่านั้นที่เคยเกิดขึ้นถ้าการแข่งขันไม่พบ)
musefan

เกิดอะไรขึ้นถ้าผู้ส่งเป็นโมฆะ? GetType จะส่งข้อยกเว้น
Jon

สองข้อเสนอแนะ: จัดการกับแหล่งที่มาเป็นโมฆะโดยการโทรเริ่มต้นหรือโยนข้อยกเว้นและกำจัดบูลีนในCaseInfoโดยเพียงแค่ตรวจสอบกับค่าประเภท (ถ้าเป็นโมฆะมันเป็นค่าเริ่มต้น)
เฟลิกซ์เค

291

ด้วย C # 7ซึ่งมาพร้อมกับ Visual Studio 2017 (รุ่น 15 *) คุณสามารถใช้ประเภทในcaseข้อความสั่ง (การจับคู่รูปแบบ):

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 # 6 คุณสามารถใช้คำสั่ง switch กับตัวดำเนินการnameof () (ขอบคุณ @Joey Adams):

switch(o.GetType().Name) {
    case nameof(AType):
        break;
    case nameof(BType):
        break;
}

ด้วย C # 5 และก่อนหน้านี้คุณสามารถใช้คำสั่ง switch แต่คุณจะต้องใช้เวทมนต์ที่มีชื่อประเภท ... ซึ่งไม่ได้เป็นมิตรกับ refactor โดยเฉพาะ (ขอบคุณ @nukefusion)

switch(o.GetType().Name) {
  case "AType":
    break;
}

1
สิ่งนี้ใช้ได้กับ case type (สตริง) .Name: ... หรือต้องใช้กับ Valuetype ด้วยหรือไม่
Tomer W

3
การทำให้
งง

6
@nukefusion: นั่นคือถ้าคุณใช้ใหม่เงาผู้ประกอบการnameof()
Joey Adams

21
ฉันไม่ชอบคำตอบนี้เนื่องจาก nameof (NamespaceA.ClassC) == nameof (NamespaceB.ClassC) เป็นจริง
ischas

7
(c # 7) คุณสามารถใช้ขีดล่างถ้าคุณไม่ต้องการเข้าถึงวัตถุ:case UnauthorizedException _:
Assaf S.

101

ตัวเลือกหนึ่งคือให้มีพจนานุกรมจากTypeถึงAction(หรือผู้รับมอบสิทธิ์อื่น ๆ ) ค้นหาการดำเนินการตามชนิดแล้วดำเนินการ ฉันเคยใช้สิ่งนี้กับโรงงานก่อนหน้านี้


31
หมายเหตุเล็กน้อย: เหมาะสำหรับการจับคู่ 1: 1 แต่อาจมีความเจ็บปวดกับการสืบทอดและ / หรืออินเทอร์เฟซ - โดยเฉพาะอย่างยิ่งเมื่อคำสั่งซื้อไม่รับประกันว่าจะถูกเก็บไว้ในพจนานุกรม แต่ถึงกระนั้นก็เป็นวิธีที่ฉันทำในสถานที่ไม่กี่แห่ง ;-p ดังนั้น +1
Marc Gravell

@Marc: การสืบทอดหรืออินเทอร์เฟซจะแตกในกระบวนทัศน์นี้อย่างไร สมมติว่าคีย์เป็นประเภทและการดำเนินการเป็นวิธีการสืบทอดหรืออินเทอร์เฟซควรบังคับ Right Thing (TM) ให้ไกลที่สุดเท่าที่ฉันจะบอกได้ ฉันเข้าใจปัญหาอย่างแน่นอนด้วยการทำงานหลายอย่างและขาดการสั่งซื้อ
Harper Shelby

2
ผมเคยใช้เทคนิคนี้มากในอดีตที่ผ่านมามักจะก่อนที่จะย้ายไปยังภาชนะ IOC
คริสคลอง

4
เทคนิคนี้แบ่งย่อยสำหรับการสืบทอดและอินเทอร์เฟซเนื่องจากคุณต้องการการโต้ตอบแบบหนึ่งต่อหนึ่งระหว่างวัตถุที่คุณกำลังตรวจสอบและผู้รับมอบสิทธิ์ที่คุณโทร คุณควรพยายามหาส่วนต่อประสานหลายอันของวัตถุใดในพจนานุกรม
Robert Rossney

5
หากคุณกำลังสร้างพจนานุกรมเพื่อจุดประสงค์นี้โดยเฉพาะคุณสามารถโอเวอร์โหลดตัวทำดัชนีเพื่อส่งคืนค่าของประเภทคีย์หรือหากหายไปซูเปอร์คลาสของมันหากหายไปซูเปอร์คลาสนั้นเป็นต้นจนกว่าจะไม่มีอะไรเหลือ
Erik Forbes

49

ด้วยคำตอบของ JaredParที่ด้านหลังของหัวของฉันฉันเขียนตัวแปรของTypeSwitchคลาสของเขาที่ใช้การอนุมานประเภทสำหรับไวยากรณ์ดีกว่า:

class A { string Name { get; } }
class B : A { string LongName { get; } }
class C : A { string FullName { get; } }
class X { public string ToString(IFormatProvider provider); }
class Y { public string GetIdentifier(); }

public string GetName(object value)
{
    string name = null;
    TypeSwitch.On(value)
        .Case((C x) => name = x.FullName)
        .Case((B x) => name = x.LongName)
        .Case((A x) => name = x.Name)
        .Case((X x) => name = x.ToString(CultureInfo.CurrentCulture))
        .Case((Y x) => name = x.GetIdentifier())
        .Default((x) => name = x.ToString());
    return name;
}

โปรดทราบว่าลำดับของCase()วิธีการมีความสำคัญ


รับโค้ดเต็มรูปแบบและแสดงความคิดเห็นสำหรับฉันTypeSwitchระดับ นี่เป็นเวอร์ชั่นย่อที่ใช้งานได้:

public static class TypeSwitch
{
    public static Switch<TSource> On<TSource>(TSource value)
    {
        return new Switch<TSource>(value);
    }

    public sealed class Switch<TSource>
    {
        private readonly TSource value;
        private bool handled = false;

        internal Switch(TSource value)
        {
            this.value = value;
        }

        public Switch<TSource> Case<TTarget>(Action<TTarget> action)
            where TTarget : TSource
        {
            if (!this.handled && this.value is TTarget)
            {
                action((TTarget) this.value);
                this.handled = true;
            }
            return this;
        }

        public void Default(Action<TSource> action)
        {
            if (!this.handled)
                action(this.value);
        }
    }
}

ดูเหมือนว่าเป็นทางออกที่ดีและต้องการดูว่าคุณจะพูดอะไรเกี่ยวกับเรื่องนี้ แต่บล็อกเสียชีวิต
Wes Grant

1
อ้อคุณพูดถูก เว็บโฮสต์ของฉันมีปัญหาบางอย่างตั้งแต่หนึ่งชั่วโมง พวกเขากำลังทำงานอยู่ โพสต์ในบล็อกของฉันนั้นเป็นคำตอบเดียวกับที่นี่ แต่มีลิงก์ไปยังซอร์สโค้ดแบบเต็ม
Daniel AA Pelsmaeker

1
รักวิธีนี้ลดพวงถ้าวงเล็บเป็นสวิตช์ "ทำงาน" ง่าย ๆ เยี่ยมมาก!
James White

2
public static Switch<TSource> Case<TSource, TTarget>(this TSource value, Action<TTarget> action) where TTarget : TSourceนอกจากนี้คุณยังสามารถเพิ่มวิธีขยายสำหรับกรณีครั้งแรก: สิ่งนี้ช่วยให้คุณสามารถพูดได้value.Case((C x) ...
Joey Adams

1
@JoeyAdams: ฉันได้รวมข้อเสนอแนะล่าสุดของคุณพร้อมกับการปรับปรุงเล็กน้อย อย่างไรก็ตามฉันยังคงไวยากรณ์เหมือนเดิม
Daniel AA Pelsmaeker

14

สร้างซูเปอร์คลาส (S) และทำให้ A และ B สืบทอดจากมัน จากนั้นประกาศวิธีการนามธรรมบน S ที่คลาสย่อยทุกคลาสจำเป็นต้องใช้

การทำเช่นนี้วิธี "foo" ยังสามารถเปลี่ยนลายเซ็นเป็น Foo (S o) ทำให้พิมพ์ปลอดภัยและคุณไม่จำเป็นต้องทิ้งข้อยกเว้นที่น่าเกลียด


จริง ๆ บรูโน่ แต่คำถามไม่ได้แนะนำว่า คุณสามารถรวมไว้ในคำตอบของคุณแม้ว่า Pablo
Dana the Sane

จากคำถามที่ฉันคิดว่า A และ B เป็นเรื่องธรรมดาพอที่จะเป็น A = String; B = รายการ <int> เช่น ...
bruno conde

13

คุณสามารถใช้การจับคู่รูปแบบใน C # 7 หรือสูงกว่า:

switch (foo.GetType())
{
    case var type when type == typeof(Player):
        break;
    case var type when type == typeof(Address):
        break;
    case var type when type == typeof(Department):
        break;
    case var type when type == typeof(ContactType):
        break;
    default:
        break;
}

ขอบคุณสำหรับสิ่งนี้! สามารถใช้ในการตรวจสอบคลาสย่อยได้เช่นกัน: if (this.TemplatedParent.GetType () IsSubclassOf (typeof (RadGridView))) สามารถเปลี่ยนเป็น: สวิตช์ (this.TemplatedParent.GetType ()) กรณีย่อย subRadGridView เมื่อ subRadGridView typeof (RadGridView)):
Flemming Bonde Kentved

คุณกำลังทำผิด ดูคำตอบโดย Serge Internและอ่านเกี่ยวกับหลักการทดแทน Liskov
0xF

8

คุณควรจะใช้วิธีการของคุณมากจนเกินไปไม่พยายามทำให้ตัวเองมัวหมอง คำตอบส่วนใหญ่ไม่ได้คำนึงถึง subclasses ในอนาคตซึ่งอาจนำไปสู่ปัญหาการบำรุงรักษาที่แย่มากในภายหลัง


3
ความละเอียดเกินพิกัดจะถูกกำหนดแบบคงที่ดังนั้นมันจะไม่ทำงานเลย
Neutrino

@ Neutrino: ไม่มีอะไรในคำถามที่บอกว่าไม่ทราบชนิดของเวลารวบรวม และถ้าเป็นเช่นนั้นการโอเวอร์โหลดทำให้รู้สึกมากกว่าตัวเลือกอื่น ๆ ตามตัวอย่างรหัสต้นฉบับของ OP
Peter Duniho

ฉันคิดว่าความจริงที่ว่าเขากำลังพยายามใช้คำสั่ง 'if' หรือ 'switch' เพื่อกำหนดประเภทเป็นตัวบ่งชี้ที่ชัดเจนว่าประเภทไม่ทราบเวลารวบรวม
Neutrino

@ Neutrino ฉันจำได้ว่าในขณะที่ Sergey Berezovskiy ชี้ให้เห็นว่ามีคำหลักแบบไดนามิกใน C # ซึ่งหมายถึงประเภทที่จะต้องมีการแก้ไขแบบไดนามิก (ที่รันไทม์มากกว่ารวบรวมเวลา)
Davide Cannizzo

8

หากคุณใช้ C # 4 คุณสามารถใช้ประโยชน์จากฟังก์ชั่นไดนามิกใหม่เพื่อให้ได้ทางเลือกที่น่าสนใจ ฉันไม่ได้พูดแบบนี้ดีกว่าในความเป็นจริงดูเหมือนว่ามันจะช้ากว่า แต่มีความสง่างามบางอย่าง

class Thing
{

  void Foo(A a)
  {
     a.Hop();
  }

  void Foo(B b)
  {
     b.Skip();
  }

}

และการใช้งาน:

object aOrB = Get_AOrB();
Thing t = GetThing();
((dynamic)t).Foo(aorB);

เหตุผลที่ใช้งานได้คือการเรียกใช้เมธอดไดนามิก C # 4 มีการแก้ไขโอเวอร์โหลดในรันไทม์แทนที่จะรวบรวมเวลา ฉันเขียนเพิ่มเติมเกี่ยวกับความคิดนี้เล็กน้อยเมื่อเร็ว ๆ นี้ อีกครั้งฉันอยากจะย้ำว่าสิ่งนี้อาจทำงานได้แย่กว่าคำแนะนำอื่น ๆ ทั้งหมดฉันกำลังเสนอมันเป็นความอยากรู้อยากเห็น


1
วันนี้ฉันมีความคิดเดียวกัน ช้ากว่าการเปลี่ยนชื่อประเภทประมาณ 3 เท่า แน่นอนว่าช้ากว่านั้นคือญาติ (สำหรับการโทร 60,000,000 ครั้ง, เพียง 4 วินาที), และโค้ดนั้นอ่านได้ง่ายกว่ามากและคุ้มค่า
Daryl

8

ใช่ขอบคุณ C # 7 ที่สามารถทำได้ นี่เป็นวิธีที่ทำได้ (ใช้รูปแบบการแสดงออก ):

switch (o)
{
    case A a:
        a.Hop();
        break;
    case B b:
        b.Skip();
        break;
    case C _: 
        return new ArgumentException("Type C will be supported in the next version");
    default:
        return new ArgumentException("Unexpected type: " + o.GetType());
}


7

สำหรับชนิดในตัวคุณสามารถใช้การแจงนับ TypeCode โปรดทราบว่า GetType () นั้นช้า แต่อาจไม่เกี่ยวข้องในสถานการณ์ส่วนใหญ่

switch (Type.GetTypeCode(someObject.GetType()))
{
    case TypeCode.Boolean:
        break;
    case TypeCode.Byte:
        break;
    case TypeCode.Char:
        break;
}

สำหรับประเภทที่กำหนดเองคุณสามารถสร้างการแจงนับของคุณเองและอินเทอร์เฟซหรือคลาสพื้นฐานที่มีคุณสมบัตินามธรรมหรือวิธีการ ...

การใช้งานระดับนามธรรมของคุณสมบัติ

public enum FooTypes { FooFighter, AbbreviatedFool, Fubar, Fugu };
public abstract class Foo
{
    public abstract FooTypes FooType { get; }
}
public class FooFighter : Foo
{
    public override FooTypes FooType { get { return FooTypes.FooFighter; } }
}

การใช้งานระดับนามธรรมของวิธีการ

public enum FooTypes { FooFighter, AbbreviatedFool, Fubar, Fugu };
public abstract class Foo
{
    public abstract FooTypes GetFooType();
}
public class FooFighter : Foo
{
    public override FooTypes GetFooType() { return FooTypes.FooFighter; }
}

การใช้อินเตอร์เฟสของคุณสมบัติ

public enum FooTypes { FooFighter, AbbreviatedFool, Fubar, Fugu };
public interface IFooType
{
    FooTypes FooType { get; }
}
public class FooFighter : IFooType
{
    public FooTypes FooType { get { return FooTypes.FooFighter; } }
}

การใช้อินเตอร์เฟสของวิธีการ

public enum FooTypes { FooFighter, AbbreviatedFool, Fubar, Fugu };
public interface IFooType
{
    FooTypes GetFooType();
}
public class FooFighter : IFooType
{
    public FooTypes GetFooType() { return FooTypes.FooFighter; }
}

เพื่อนร่วมงานคนหนึ่งของฉันเพิ่งบอกฉันเกี่ยวกับเรื่องนี้เช่นกัน: นี่เป็นข้อได้เปรียบที่คุณสามารถใช้เพื่อวัตถุประเภทใดก็ได้ไม่ใช่แค่ที่คุณกำหนดไว้ มันมีข้อเสียของการเป็นใหญ่ขึ้นและช้าลง

ก่อนกำหนดคลาสแบบคงที่เช่นนี้:

public static class TypeEnumerator
{
    public class TypeEnumeratorException : Exception
    {
        public Type unknownType { get; private set; }
        public TypeEnumeratorException(Type unknownType) : base()
        {
            this.unknownType = unknownType;
        }
    }
    public enum TypeEnumeratorTypes { _int, _string, _Foo, _TcpClient, };
    private static Dictionary<Type, TypeEnumeratorTypes> typeDict;
    static TypeEnumerator()
    {
        typeDict = new Dictionary<Type, TypeEnumeratorTypes>();
        typeDict[typeof(int)] = TypeEnumeratorTypes._int;
        typeDict[typeof(string)] = TypeEnumeratorTypes._string;
        typeDict[typeof(Foo)] = TypeEnumeratorTypes._Foo;
        typeDict[typeof(System.Net.Sockets.TcpClient)] = TypeEnumeratorTypes._TcpClient;
    }
    /// <summary>
    /// Throws NullReferenceException and TypeEnumeratorException</summary>
    /// <exception cref="System.NullReferenceException">NullReferenceException</exception>
    /// <exception cref="MyProject.TypeEnumerator.TypeEnumeratorException">TypeEnumeratorException</exception>
    public static TypeEnumeratorTypes EnumerateType(object theObject)
    {
        try
        {
            return typeDict[theObject.GetType()];
        }
        catch (KeyNotFoundException)
        {
            throw new TypeEnumeratorException(theObject.GetType());
        }
    }
}

และจากนั้นคุณสามารถใช้มันเช่นนี้:

switch (TypeEnumerator.EnumerateType(someObject))
{
    case TypeEnumerator.TypeEnumeratorTypes._int:
        break;
    case TypeEnumerator.TypeEnumeratorTypes._string:
        break;
}

ขอบคุณที่เพิ่ม TypeCode () - ตัวแปรสำหรับประเภทดั้งเดิมเนื่องจากแม้แต่ C # 7.0 - ตัวแปรไม่ทำงานกับสิ่งเหล่านั้น (ทั้งชื่อไม่ชัดเจน) ()
Ole Albers

6

ฉันชอบการใช้การพิมพ์โดยนัยของ Virtlink เพื่อทำให้สวิตช์อ่านได้ง่ายขึ้นมาก แต่ฉันไม่ชอบว่าการเริ่มต้นไม่สามารถทำได้และเรากำลังทำการจัดสรร เรามาดูกันดีกว่าหน่อย

public static class TypeSwitch
{
    public static void On<TV, T1>(TV value, Action<T1> action1)
        where T1 : TV
    {
        if (value is T1) action1((T1)value);
    }

    public static void On<TV, T1, T2>(TV value, Action<T1> action1, Action<T2> action2)
        where T1 : TV where T2 : TV
    {
        if (value is T1) action1((T1)value);
        else if (value is T2) action2((T2)value);
    }

    public static void On<TV, T1, T2, T3>(TV value, Action<T1> action1, Action<T2> action2, Action<T3> action3)
        where T1 : TV where T2 : TV where T3 : TV
    {
        if (value is T1) action1((T1)value);
        else if (value is T2) action2((T2)value);
        else if (value is T3) action3((T3)value);
    }

    // ... etc.
}

นั่นทำให้นิ้วของฉันเจ็บ มาทำกันใน T4:

<#@ template debug="false" hostSpecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ Assembly Name="System.Core.dll" #>
<#@ import namespace="System.Linq" #> 
<#@ import namespace="System.IO" #> 
<#
    string GenWarning = "// THIS FILE IS GENERATED FROM " + Path.GetFileName(Host.TemplateFile) + " - ANY HAND EDITS WILL BE LOST!";
    const int MaxCases = 15;
#>
<#=GenWarning#>

using System;

public static class TypeSwitch
{
<# for(int icase = 1; icase <= MaxCases; ++icase) {
    var types = string.Join(", ", Enumerable.Range(1, icase).Select(i => "T" + i));
    var actions = string.Join(", ", Enumerable.Range(1, icase).Select(i => string.Format("Action<T{0}> action{0}", i)));
    var wheres = string.Join(" ", Enumerable.Range(1, icase).Select(i => string.Format("where T{0} : TV", i)));
#>
    <#=GenWarning#>

    public static void On<TV, <#=types#>>(TV value, <#=actions#>)
        <#=wheres#>
    {
        if (value is T1) action1((T1)value);
<# for(int i = 2; i <= icase; ++i) { #>
        else if (value is T<#=i#>) action<#=i#>((T<#=i#>)value);
<#}#>
    }

<#}#>
    <#=GenWarning#>
}

ปรับตัวอย่างของ Virtlink เล็กน้อย:

TypeSwitch.On(operand,
    (C x) => name = x.FullName,
    (B x) => name = x.LongName,
    (A x) => name = x.Name,
    (X x) => name = x.ToString(CultureInfo.CurrentCulture),
    (Y x) => name = x.GetIdentifier(),
    (object x) => name = x.ToString());

อ่านและรวดเร็ว ทีนี้เมื่อทุกคนชี้ไปที่คำตอบของพวกเขาและด้วยธรรมชาติของคำถามนี้คำสั่งนั้นสำคัญในการจับคู่ประเภท ดังนั้น:

  • ใส่ประเภทใบไม้ก่อนประเภทฐานในภายหลัง
  • สำหรับประเภทเพียร์ให้วางการจับคู่ที่มีโอกาสมากขึ้นก่อนเพื่อเพิ่มความสมบูรณ์
  • นี่ก็หมายความว่าไม่จำเป็นต้องมีตัวพิมพ์ใหญ่พิเศษ ให้ใช้ชนิดพื้นฐานมากที่สุดในแลมบ์ดาแล้วนำไปวางไว้ที่เดิม

5

การรับมรดกช่วยให้วัตถุได้รับการยอมรับว่าเป็นประเภทมากกว่าหนึ่งประเภทฉันคิดว่าสวิตช์อาจทำให้เกิดความกำกวมที่ไม่ดี ตัวอย่างเช่น:

กรณีที่ 1

{
  string s = "a";
  if (s is string) Print("Foo");
  else if (s is object) Print("Bar");
}

กรณีที่ 2

{
  string s = "a";
  if (s is object) Print("Foo");
  else if (s is string) Print("Bar");
}

เพราะ s เป็นสตริงและวัตถุ ฉันคิดว่าเมื่อคุณเขียน a switch(foo)คุณคาดหวังว่า foo จะจับคู่หนึ่งและเพียงหนึ่งในcaseงบ ด้วยสวิตช์บนชนิดลำดับที่คุณเขียนคำสั่ง case ของคุณอาจเปลี่ยนผลลัพธ์ของคำสั่ง switch ทั้งหมด ฉันคิดว่ามันจะผิด

คุณสามารถนึกถึงคอมไพเลอร์ - ตรวจสอบเกี่ยวกับประเภทของคำสั่ง "typeswitch" การตรวจสอบว่าประเภทที่ระบุไม่ได้รับมรดกจากกันและกัน ที่ไม่ได้มีอยู่แม้ว่า

foo is Tไม่เหมือนกับfoo.GetType() == typeof(T)!!


4

ฉันก็จะ

  • ใช้วิธีการมากไป (เช่นx0n ) หรือ
  • ใช้คลาสย่อย (เช่นเดียวกับPablo ) หรือ
  • ใช้รูปแบบของผู้เข้าชม

4

อีกวิธีหนึ่งคือการกำหนดส่วนต่อประสาน IThing และนำไปใช้ในทั้งสองคลาสนี่คือตัวอย่าง:

public interface IThing
{
    void Move();
}

public class ThingA : IThing
{
    public void Move()
    {
        Hop();
    }

    public void Hop(){  
        //Implementation of Hop 
    }

}

public class ThingA : IThing
{
    public void Move()
    {
        Skip();
    }

    public void Skip(){ 
        //Implementation of Skip    
    }

}

public class Foo
{
    static void Main(String[] args)
    {

    }

    private void Foo(IThing a)
    {
        a.Move();
    }
}

4

ตามข้อกำหนด C # 7.0 คุณสามารถประกาศตัวแปรท้องถิ่นที่กำหนดขอบเขตcaseของswitch:

object a = "Hello world";
switch (a)
{
    case string myString:
        // The variable 'a' is a string!
        break;
    case int myInt:
        // The variable 'a' is an int!
        break;
    case Foo myFoo:
        // The variable 'a' is of type Foo!
        break;
}

นี่เป็นวิธีที่ดีที่สุดในการทำสิ่งนี้เพราะมันเกี่ยวข้องกับการดำเนินการเพียงแค่การแคสต์และการผลักดันแบบสแต็กซึ่งเป็นการดำเนินการที่เร็วที่สุดที่ล่ามสามารถทำงานได้หลังจากการทำงานและbooleanเงื่อนไขแบบ บิต

เมื่อเปรียบเทียบสิ่งนี้กับ a Dictionary<K, V>นี่เป็นการใช้หน่วยความจำน้อยกว่ามาก: การเก็บพจนานุกรมต้องการพื้นที่ใน RAM และการคำนวณเพิ่มเติมโดย CPU สำหรับการสร้างสองอาร์เรย์ (หนึ่งสำหรับคีย์และอีกอันสำหรับค่า) และรวบรวมรหัสแฮชสำหรับคีย์ ค่าให้กับคีย์ที่เกี่ยวข้อง

ดังนั้นสำหรับเท่าที่ฉันรู้ว่าฉันไม่เชื่อว่าวิธีที่เร็วกว่าสามารถอยู่ถ้าคุณต้องการที่จะใช้เพียงif- then- elseบล็อกกับisผู้ประกอบการดังต่อไปนี้:

object a = "Hello world";
if (a is string)
{
    // The variable 'a' is a string!
} else if (a is int)
{
    // The variable 'a' is an int!
} // etc.

3

คุณสามารถสร้างวิธีการโอเวอร์โหลด:

void Foo(A a) 
{ 
    a.Hop(); 
}

void Foo(B b) 
{ 
    b.Skip(); 
}

void Foo(object o) 
{ 
    throw new ArgumentException("Unexpected type: " + o.GetType()); 
}

และโยนอาร์กิวเมนต์เพื่อdynamicพิมพ์เพื่อหลีกเลี่ยงการตรวจสอบชนิดคงที่:

Foo((dynamic)something);

3

การปรับปรุงการจับคู่รูปแบบ C # 8 ทำให้สามารถทำสิ่งนี้ได้ ในบางกรณีมันทำงานและรัดกุมมากขึ้น

        public Animal Animal { get; set; }
        ...
        var animalName = Animal switch
        {
            Cat cat => "Tom",
            Mouse mouse => "Jerry",
            _ => "unknown"
        };

2

คุณกำลังมองหาDiscriminated Unionsซึ่งเป็นคุณสมบัติภาษาของ F # แต่คุณสามารถบรรลุผลที่คล้ายกันโดยใช้ห้องสมุดที่ฉันทำเรียกว่า OneOf

https://github.com/mcintyre321/OneOf

ข้อได้เปรียบที่สำคัญเหนือกว่าswitch(และifและexceptions as control flow) คือมันปลอดภัยในการรวบรวมเวลา - ไม่มีตัวจัดการเริ่มต้นหรือล้มเหลว

void Foo(OneOf<A, B> o)
{
    o.Switch(
        a => a.Hop(),
        b => b.Skip()
    );
}

หากคุณเพิ่มรายการที่สามเป็น o คุณจะได้รับข้อผิดพลาดของคอมไพเลอร์เนื่องจากคุณต้องเพิ่มตัวจัดการ Func ในการโทรสลับ

นอกจากนี้คุณยังสามารถทำสิ่ง.Matchที่คืนค่าแทนที่จะดำเนินการคำสั่ง:

double Area(OneOf<Square, Circle> o)
{
    return o.Match(
        square => square.Length * square.Length,
        circle => Math.PI * circle.Radius * circle.Radius
    );
}

2

สร้างอินเทอร์เฟซIFooableจากนั้นให้คลาสของคุณAและBใช้วิธีการทั่วไปซึ่งจะเรียกวิธีการที่สอดคล้องกันที่คุณต้องการ:

interface IFooable
{
    public void Foo();
}

class A : IFooable
{
    //other methods ...

    public void Foo()
    {
        this.Hop();
    }
}

class B : IFooable
{
    //other methods ...

    public void Foo()
    {
        this.Skip();
    }
}

class ProcessingClass
{
    public void Foo(object o)
    {
        if (o == null)
            throw new NullRefferenceException("Null reference", "o");

        IFooable f = o as IFooable;
        if (f != null)
        {
            f.Foo();
        }
        else
        {
            throw new ArgumentException("Unexpected type: " + o.GetType());
        }
    }
}

โปรดทราบว่าจะดีกว่าที่จะใช้asแทนการตรวจสอบครั้งแรกก่อนisแล้วจึงทำการคัดเลือกตามวิธีการที่คุณสร้าง 2 casts ดังนั้นจึงมีราคาแพงกว่า


2

ฉันมักจะจบลงด้วยรายการเพรดิเคตและการกระทำ บางสิ่งบางอย่างตามสายเหล่านี้:

class Mine {
    static List<Func<object, bool>> predicates;
    static List<Action<object>> actions;

    static Mine() {
        AddAction<A>(o => o.Hop());
        AddAction<B>(o => o.Skip());
    }

    static void AddAction<T>(Action<T> action) {
        predicates.Add(o => o is T);
        actions.Add(o => action((T)o);
    }

    static void RunAction(object o) {
        for (int i=0; o < predicates.Count; i++) {
            if (predicates[i](o)) {
                actions[i](o);
                break;
            }
        }
    }

    void Foo(object o) {
        RunAction(o);
    }
}

2

หลังจากเปรียบเทียบตัวเลือกสองสามคำตอบที่ให้ไว้กับฟีเจอร์ F # ฉันค้นพบ F # เพื่อให้มีวิธีที่ดีกว่าในการสนับสนุนการสลับตามประเภท (แม้ว่าฉันยังคงยึดติดกับ C #)
คุณอาจต้องการที่จะเห็นที่นี่และที่นี่


2
<เสียบปลั๊กสำหรับ F # ที่นี่>
Overlord Zurg

1

ฉันจะสร้างอินเตอร์เฟซกับสิ่งที่ชื่อและชื่อวิธีที่จะทำให้ความรู้สึกของสวิทช์ของคุณขอเรียกพวกเขาตามลำดับ: ที่บอกว่าในการดำเนินการIDoablevoid Do()

public interface IDoable
{
    void Do();
}

public class A : IDoable
{
    public void Hop() 
    {
        // ...
    }

    public void Do()
    {
        Hop();
    }
}

public class B : IDoable
{
    public void Skip() 
    {
        // ...
    }

    public void Do()
    {
        Skip();
    }
}

และเปลี่ยนวิธีการดังต่อไปนี้:

void Foo<T>(T obj)
    where T : IDoable
{
    // ...
    obj.Do();
    // ...
}

อย่างน้อยเมื่อคุณมีความปลอดภัยในการรวบรวมเวลาและฉันสงสัยว่าประสิทธิภาพที่ชาญฉลาดจะดีกว่าการตรวจสอบประเภทที่รันไทม์


1

ด้วย C # 8 เป็นต้นไปคุณสามารถทำให้มันกระชับยิ่งขึ้นกับสวิตช์ใหม่ และด้วยการใช้ตัวเลือกการทิ้ง _ คุณสามารถหลีกเลี่ยงการสร้างตัวแปร innecesary เมื่อคุณไม่ต้องการตัวแปรเช่นนี้:

        return document switch {
            Invoice _ => "Is Invoice",
            ShippingList _ => "Is Shipping List",
            _ => "Unknown"
        };

ใบแจ้งหนี้และ ShippingList เป็นคลาสและเอกสารเป็นวัตถุที่สามารถเป็นได้


0

ฉันเห็นด้วยกับจอนเกี่ยวกับการแฮชของการกระทำกับชื่อคลาส หากคุณรักษารูปแบบของคุณไว้คุณอาจต้องพิจารณาใช้โครงสร้าง "เป็น" แทน:

A a = o as A;
if (a != null) {
    a.Hop();
    return;
}
B b = o as B;
if (b != null) {
    b.Skip();
    return;
}
throw new ArgumentException("...");

ความแตกต่างก็คือเมื่อคุณใช้รูปแบบถ้า (foo is Bar) {((Bar) foo) .Action (); } คุณกำลังทำการคัดเลือกนักแสดงอยู่สองครั้ง ตอนนี้คอมไพเลอร์อาจปรับให้เหมาะสมและทำงานเพียงครั้งเดียว - แต่ฉันจะไม่นับมัน


1
ฉันไม่ชอบจุดทางออกหลายจุด (คืน) แต่ถ้าคุณต้องการติดกับสิ่งนี้เพิ่ม "ถ้า (o == null) โยน" ในการเริ่มต้นหลังจากนั้นคุณจะไม่ทราบว่านักแสดงไม่ประสบความสำเร็จหรือ วัตถุนั้นเป็นโมฆะ
Sunny Milenov

0

ดังที่ Pablo แนะนำวิธีการเชื่อมต่อเป็นสิ่งที่ถูกต้องในการจัดการสิ่งนี้ เพื่อใช้สวิตช์จริง ๆ อีกทางเลือกหนึ่งคือให้ enum แบบกำหนดเองแสดงถึงประเภทของคุณในคลาสของคุณ

enum ObjectType { A, B, Default }

interface IIdentifiable
{
    ObjectType Type { get; };
}
class A : IIdentifiable
{
    public ObjectType Type { get { return ObjectType.A; } }
}

class B : IIdentifiable
{
    public ObjectType Type { get { return ObjectType.B; } }
}

void Foo(IIdentifiable o)
{
    switch (o.Type)
    {
        case ObjectType.A:
        case ObjectType.B:
        //......
    }
}

นี่คือประเภทของการดำเนินการใน BCL เช่นกัน ตัวอย่างหนึ่งคือMemberInfo.MemberTypesและอีกประเภทหนึ่งGetTypeCodeสำหรับประเภทดั้งเดิมเช่น:

void Foo(object o)
{
    switch (Type.GetTypeCode(o.GetType())) // for IConvertible, just o.GetTypeCode()
    {
        case TypeCode.Int16:
        case TypeCode.Int32:
        //etc ......
    }
}

0

นี่คือคำตอบสำรองที่ผสมผสานการมีส่วนร่วมจากคำตอบ JaredPar และ VirtLink ด้วยข้อ จำกัด ดังต่อไปนี้:

  • โครงสร้างสวิตช์ทำหน้าที่เป็นฟังก์ชันและรับฟังก์ชั่นเป็นพารามิเตอร์เคส
  • ตรวจสอบให้แน่ใจว่ามันถูกสร้างขึ้นอย่างถูกต้องและมีฟังก์ชั่นเริ่มต้นอยู่เสมอฟังก์ชั่นเริ่มต้น
  • มันกลับมาหลังจากการแข่งขันครั้งแรก (จริงสำหรับคำตอบ JaredPar ไม่เป็นความจริงสำหรับ VirtLink หนึ่ง)

การใช้งาน:

 var result = 
   TSwitch<string>
     .On(val)
     .Case((string x) => "is a string")
     .Case((long x) => "is a long")
     .Default(_ => "what is it?");

รหัส:

public class TSwitch<TResult>
{
    class CaseInfo<T>
    {
        public Type Target { get; set; }
        public Func<object, T> Func { get; set; }
    }

    private object _source;
    private List<CaseInfo<TResult>> _cases;

    public static TSwitch<TResult> On(object source)
    {
        return new TSwitch<TResult> { 
            _source = source,
            _cases = new List<CaseInfo<TResult>>()
        };
    }

    public TResult Default(Func<object, TResult> defaultFunc)
    {
        var srcType = _source.GetType();
       foreach (var entry in _cases)
            if (entry.Target.IsAssignableFrom(srcType))
                return entry.Func(_source);

        return defaultFunc(_source);
    }

    public TSwitch<TResult> Case<TSource>(Func<TSource, TResult> func)
    {
        _cases.Add(new CaseInfo<TResult>
        {
            Func = x => func((TSource)x),
            Target = typeof(TSource)
        });
        return this;
    }
}

0

ใช่ - เพียงใช้ "รูปแบบการจับคู่รูปแบบ" ที่แปลกประหลาดจาก C # 7 ขึ้นไปเพื่อจับคู่กับคลาสหรือโครงสร้าง:

IObject concrete1 = new ObjectImplementation1();
IObject concrete2 = new ObjectImplementation2();

switch (concrete1)
{
    case ObjectImplementation1 c1: return "type 1";         
    case ObjectImplementation2 c2: return "type 2";         
}

0

ฉันใช้

    public T Store<T>()
    {
        Type t = typeof(T);

        if (t == typeof(CategoryDataStore))
            return (T)DependencyService.Get<IDataStore<ItemCategory>>();
        else
            return default(T);
    }

0

ควรทำงานด้วย

ชนิดเคส _:

ชอบ:

int i = 1;
bool b = true;
double d = 1.1;
object o = i; // whatever you want

switch (o)
{
    case int _:
        Answer.Content = "You got the int";
        break;
    case double _:
        Answer.Content = "You got the double";
        break;
    case bool _:
        Answer.Content = "You got the bool";
        break;
}

0

หากคุณรู้ชั้นเรียนที่คุณคาดหวัง แต่คุณยังไม่มีวัตถุคุณสามารถทำสิ่งนี้ได้:

private string GetAcceptButtonText<T>() where T : BaseClass, new()
{
    switch (new T())
    {
        case BaseClassReview _: return "Review";
        case BaseClassValidate _: return "Validate";
        case BaseClassAcknowledge _: return "Acknowledge";
        default: return "Accept";
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.