สร้างวิธีการทั่วไป จำกัด T เพื่อ Enum


1188

ฉันกำลังสร้างฟังก์ชั่นเพื่อขยายEnum.Parseแนวคิดที่ว่า

  • อนุญาตให้แยกวิเคราะห์ค่าเริ่มต้นในกรณีที่ไม่พบค่า Enum
  • เป็นกรณีตาย

ดังนั้นฉันจึงเขียนสิ่งต่อไปนี้:

public static T GetEnumFromString<T>(string value, T defaultValue) where T : Enum
{
    if (string.IsNullOrEmpty(value)) return defaultValue;
    foreach (T item in Enum.GetValues(typeof(T)))
    {
        if (item.ToString().ToLower().Equals(value.Trim().ToLower())) return item;
    }
    return defaultValue;
}

ฉันได้รับข้อผิดพลาดไม่สามารถเป็นคลาสพิเศษSystem.Enumได้

ยุติธรรมเพียงพอ แต่มีวิธีแก้ปัญหาเพื่อให้ Generic Enum หรือฉันจะต้องเลียนแบบParseฟังก์ชั่นและส่งประเภทเป็นแอตทริบิวต์ซึ่งบังคับให้ความต้องการมวยที่น่าเกลียดในรหัสของคุณ

แก้ไขข้อเสนอแนะด้านล่างทั้งหมดได้รับการชื่นชมอย่างมากขอบคุณ

ตั้งรกรากอยู่ที่ (ฉันออกจากลูปเพื่อรักษาความรู้สึกตัวพิมพ์เล็กและตัวพิมพ์ใหญ่ - ฉันใช้นี่เมื่อแยกวิเคราะห์ XML)

public static class EnumUtils
{
    public static T ParseEnum<T>(string value, T defaultValue) where T : struct, IConvertible
    {
        if (!typeof(T).IsEnum) throw new ArgumentException("T must be an enumerated type");
        if (string.IsNullOrEmpty(value)) return defaultValue;

        foreach (T item in Enum.GetValues(typeof(T)))
        {
            if (item.ToString().ToLower().Equals(value.Trim().ToLower())) return item;
        }
        return defaultValue;
    }
}

แก้ไข: (16 กุมภาพันธ์ 2558) Julien Lebosquain เพิ่งโพสต์คอมไพเลอร์บังคับใช้ประเภททั่วไปที่ปลอดภัยประเภทการแก้ปัญหาใน MSIL หรือ F #ด้านล่างซึ่งคุ้มค่าดูและ upvote ฉันจะลบการแก้ไขนี้หากวิธีแก้ไขปัญหาฟองสบู่ขึ้นหน้า


10
บางทีคุณควรใช้ ToUpperInvariant () แทน ToLower () ...
Max Galkin

31
@Shimmy: ทันทีที่คุณส่งประเภทค่าไปยังวิธีการขยายคุณกำลังทำงานกับสำเนาของมันดังนั้นคุณจึงไม่สามารถเปลี่ยนสถานะได้
Garo Yeriazarian

4
รู้ว่ามันเป็นเธรดเก่าไม่ทราบว่าพวกเขาเปลี่ยนสิ่งต่าง ๆ หรือไม่ แต่วิธีการขยายใช้งานได้ดีสำหรับประเภทค่าแน่ใจว่าพวกเขาอาจไม่สมเหตุสมผลเสมอไป แต่ฉันได้ใช้ "สาธารณะคงที่ TimeSpan วินาที (int x) นี้ return TimeSpan.FromSeconds (x);} "เพื่อเปิดใช้งานไวยากรณ์ของ" Wait.For (5.Seconds ()) ... "
Jens

6
ตระหนักดีว่านี่ไม่ใช่ส่วนหนึ่งของคำถาม แต่คุณสามารถปรับปรุงตรรกะวงรอบหน้าของคุณโดยใช้ String.Equals กับ StringComparison.InvariantCultureIgnoreCase
Firestrand

คำตอบ:


1006

เนื่องจากEnumType ใช้IConvertibleอินเตอร์เฟสการใช้งานที่ดีควรเป็นดังนี้:

public T GetEnumFromString<T>(string value) where T : struct, IConvertible
{
   if (!typeof(T).IsEnum) 
   {
      throw new ArgumentException("T must be an enumerated type");
   }

   //...
}

IConvertibleนี้จะยังคงอนุญาตให้มีการส่งผ่านของประเภทค่าดำเนินการ โอกาสมีน้อยมาก


2
ข้อมูลทั่วไปมีให้ตั้งแต่. NET 2.0 ดังนั้นพวกเขาจึงมีอยู่ใน vb 2005 เช่นกัน
Vivek

46
ถ้าหากคุณเลือกที่จะลงเส้นทางนี้ให้ใช้ "class TestClass <T> โดยที่ T: struct, IComparable, IFormattable, IConvertible"
Ricardo Nolde

106
ข้อเสนอแนะอีกประการหนึ่งคือการกำหนดประเภททั่วไปด้วยตัวระบุ TEnum ดังนั้น: TEnum สาธารณะ GetEnumFromString <TEnum> (ค่าสตริง) โดยที่ TEnum: struct, IConvertible, IComparible, IFormattable {}
Lisa

11
คุณไม่ได้รับมากนักโดยรวมถึงอินเทอร์เฟซอื่น ๆ เพราะเกือบทุกชนิดของมูลค่าในตัวใช้อินเทอร์เฟซเหล่านั้น นี่เป็นความจริงโดยเฉพาะอย่างยิ่งสำหรับข้อ จำกัด ในวิธีการขยายทั่วไปซึ่งมีประโยชน์อย่างมากสำหรับการใช้งานกับ enums ยกเว้นความจริงที่ว่าวิธีการขยายเหล่านั้นเป็นเหมือนไวรัสที่ติดเชื้อวัตถุทั้งหมดของคุณ IConvertable อย่างน้อยก็แคบลงเล็กน้อย
russbishop

2
@SamIam: เมื่อคุณโพสต์กระทู้นี้คืออะไร 6 และครึ่งปีและคุณถูกต้องไม่มีการตรวจสอบเวลาคอมไพล์ในคำตอบใด ๆ จากนั้นเพียง 3 วันต่อมาหลังจาก 6 ปีคุณจะได้รับความปรารถนาของคุณ - ดูวิธีการโพสต์ของ Julien Lebosquain ด้านล่าง
David I. McIntosh

662

ในที่สุดคุณสมบัตินี้ก็ได้รับการสนับสนุนใน C # 7.3!

ตัวอย่างต่อไปนี้ (จากตัวอย่าง dotnet ) แสดงวิธี:

public static Dictionary<int, string> EnumNamedValues<T>() where T : System.Enum
{
    var result = new Dictionary<int, string>();
    var values = Enum.GetValues(typeof(T));

    foreach (int item in values)
        result.Add(item, Enum.GetName(typeof(T), item));
    return result;
}

ตรวจสอบให้แน่ใจว่าได้ตั้งค่าเวอร์ชันภาษาของคุณในโปรเจ็กต์ C # ของคุณเป็นเวอร์ชัน 7.3


คำตอบเดิมด้านล่าง:

ฉันไปเล่นเกมช้า แต่ฉันถือเป็นความท้าทายที่จะเห็นว่ามันสามารถทำได้ มันเป็นไปไม่ได้ใน C # (หรือ VB.NET แต่เลื่อนลงเพื่อ F #) แต่เป็นไปได้ใน MSIL ฉันเขียนสิ่งเล็กน้อย ....

// license: http://www.apache.org/licenses/LICENSE-2.0.html
.assembly MyThing{}
.class public abstract sealed MyThing.Thing
       extends [mscorlib]System.Object
{
  .method public static !!T  GetEnumFromString<valuetype .ctor ([mscorlib]System.Enum) T>(string strValue,
                                                                                          !!T defaultValue) cil managed
  {
    .maxstack  2
    .locals init ([0] !!T temp,
                  [1] !!T return_value,
                  [2] class [mscorlib]System.Collections.IEnumerator enumerator,
                  [3] class [mscorlib]System.IDisposable disposer)
    // if(string.IsNullOrEmpty(strValue)) return defaultValue;
    ldarg strValue
    call bool [mscorlib]System.String::IsNullOrEmpty(string)
    brfalse.s HASVALUE
    br RETURNDEF         // return default it empty

    // foreach (T item in Enum.GetValues(typeof(T)))
  HASVALUE:
    // Enum.GetValues.GetEnumerator()
    ldtoken !!T
    call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
    call class [mscorlib]System.Array [mscorlib]System.Enum::GetValues(class [mscorlib]System.Type)
    callvirt instance class [mscorlib]System.Collections.IEnumerator [mscorlib]System.Array::GetEnumerator() 
    stloc enumerator
    .try
    {
      CONDITION:
        ldloc enumerator
        callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
        brfalse.s LEAVE

      STATEMENTS:
        // T item = (T)Enumerator.Current
        ldloc enumerator
        callvirt instance object [mscorlib]System.Collections.IEnumerator::get_Current()
        unbox.any !!T
        stloc temp
        ldloca.s temp
        constrained. !!T

        // if (item.ToString().ToLower().Equals(value.Trim().ToLower())) return item;
        callvirt instance string [mscorlib]System.Object::ToString()
        callvirt instance string [mscorlib]System.String::ToLower()
        ldarg strValue
        callvirt instance string [mscorlib]System.String::Trim()
        callvirt instance string [mscorlib]System.String::ToLower()
        callvirt instance bool [mscorlib]System.String::Equals(string)
        brfalse.s CONDITION
        ldloc temp
        stloc return_value
        leave.s RETURNVAL

      LEAVE:
        leave.s RETURNDEF
    }
    finally
    {
        // ArrayList's Enumerator may or may not inherit from IDisposable
        ldloc enumerator
        isinst [mscorlib]System.IDisposable
        stloc.s disposer
        ldloc.s disposer
        ldnull
        ceq
        brtrue.s LEAVEFINALLY
        ldloc.s disposer
        callvirt instance void [mscorlib]System.IDisposable::Dispose()
      LEAVEFINALLY:
        endfinally
    }

  RETURNDEF:
    ldarg defaultValue
    stloc return_value

  RETURNVAL:
    ldloc return_value
    ret
  }
} 

ซึ่งสร้างฟังก์ชั่นที่จะมีหน้าตาแบบนี้ถ้ามันถูกต้อง C #:

T GetEnumFromString<T>(string valueString, T defaultValue) where T : Enum

จากนั้นด้วยรหัส C # ต่อไปนี้:

using MyThing;
// stuff...
private enum MyEnum { Yes, No, Okay }
static void Main(string[] args)
{
    Thing.GetEnumFromString("No", MyEnum.Yes); // returns MyEnum.No
    Thing.GetEnumFromString("Invalid", MyEnum.Okay);  // returns MyEnum.Okay
    Thing.GetEnumFromString("AnotherInvalid", 0); // compiler error, not an Enum
}

น่าเสียดายที่นี่หมายถึงการมีส่วนหนึ่งของรหัสของคุณเขียนใน MSIL แทนที่จะเป็น C # โดยมีประโยชน์เพิ่มเติมเพียงอย่างเดียวคือคุณสามารถ จำกัด วิธีการนี้System.Enumได้ นอกจากนี้ยังเป็นคนเกียจคร้านเพราะได้รับการรวบรวมเป็นชุดแยกต่างหาก อย่างไรก็ตามไม่ได้หมายความว่าคุณจะต้องปรับใช้ด้วยวิธีดังกล่าว

โดยการลบบรรทัดออก.assembly MyThing{}และเรียกใช้อุลลัมม์ดังต่อไปนี้:

ilasm.exe /DLL /OUTPUT=MyThing.netmodule

คุณได้รับ netmodule แทนที่จะเป็นชุดประกอบ

น่าเสียดายที่ VS2010 (และก่อนหน้านี้เห็นได้ชัด) ไม่สนับสนุนการเพิ่มการอ้างอิง netmodule ซึ่งหมายความว่าคุณจะต้องทิ้งไว้ใน 2 แอสเซมบลีที่แยกต่างหากเมื่อคุณทำการดีบัก วิธีเดียวที่คุณสามารถเพิ่มพวกเขาเป็นส่วนหนึ่งของการชุมนุมของคุณจะเรียกใช้ csc.exe ด้วยตัวคุณเองโดยใช้/addmodule:{files}อาร์กิวเมนต์บรรทัดคำสั่ง มันจะไม่เจ็บปวดเกินไปในสคริปต์ MSBuild แน่นอนถ้าคุณกล้าหรือโง่คุณสามารถเรียกใช้ csc ด้วยตนเองทุกครั้ง และแน่นอนว่ามันซับซ้อนมากขึ้นเนื่องจากแอสเซมบลีหลายตัวจำเป็นต้องเข้าถึง

ดังนั้นมันสามารถทำได้ในสุทธิ มันคุ้มค่ากับความพยายามพิเศษหรือไม่? อืมฉันคิดว่าฉันจะให้คุณตัดสินใจ


F # โซลูชั่นเป็นทางเลือก

เครดิตพิเศษ: ปรากฎว่ามีข้อ จำกัด ทั่วไปที่enumเป็นไปได้ในภาษา. NET อย่างน้อยหนึ่งภาษานอกเหนือจาก MSIL: F #

type MyThing =
    static member GetEnumFromString<'T when 'T :> Enum> str defaultValue: 'T =
        /// protect for null (only required in interop with C#)
        let str = if isNull str then String.Empty else str

        Enum.GetValues(typedefof<'T>)
        |> Seq.cast<_>
        |> Seq.tryFind(fun v -> String.Compare(v.ToString(), str.Trim(), true) = 0)
        |> function Some x -> x | None -> defaultValue

อันนี้ง่ายต่อการบำรุงรักษาเนื่องจากเป็นภาษาที่มีชื่อเสียงพร้อมการสนับสนุน Visual Studio IDE เต็มรูปแบบ แต่คุณยังต้องการโครงการแยกต่างหากในโซลูชันของคุณ แต่มันเป็นธรรมชาติผลิต IL ที่แตกต่างกันมาก (รหัสเป็นที่แตกต่างกันมาก) และอาศัยอยู่กับFSharp.Coreห้องสมุดซึ่งเช่นเดียวกับห้องสมุดภายนอกอื่น ๆ ความต้องการที่จะเป็นส่วนหนึ่งของการกระจายของคุณ

นี่คือวิธีที่คุณสามารถใช้ (โดยพื้นฐานเหมือนกับโซลูชัน MSIL) และเพื่อแสดงว่ามันล้มเหลวอย่างถูกต้องใน structs ที่มีความหมายเหมือนกัน:

// works, result is inferred to have type StringComparison
var result = MyThing.GetEnumFromString("OrdinalIgnoreCase", StringComparison.Ordinal);
// type restriction is recognized by C#, this fails at compile time
var result = MyThing.GetEnumFromString("OrdinalIgnoreCase", 42);

67
ใช่ไม่ยอมใครง่ายๆมาก ฉันมีความเคารพอย่างสูงสุดสำหรับผู้ที่สามารถเขียนโค้ดใน IL และรู้ว่าคุณลักษณะนี้ได้รับการสนับสนุนในระดับภาษาที่สูงขึ้นอย่างไร - ระดับที่เราหลายคนยังเห็นว่าอยู่ในระดับต่ำภายใต้แอปพลิเคชันกฎเกณฑ์ทางธุรกิจ .
TonyG

13
สิ่งที่ฉันอยากรู้คือเหตุผลที่ทีม C # ยังไม่เริ่มให้สิ่งนี้เนื่องจาก MSIL ได้รับการสนับสนุนแล้ว
MgSam

25
@MgSam - จากEric Lippert :There's no particularly unusual reason why not; we have lots of other things to do, limited budgets, and this one has never made it past the "wouldn't this be nice?" discussion in the language design team.
Christopher Currens

5
@ LordofScripts: ฉันคิดว่าเหตุผลก็คือว่าตั้งแต่ชั้นเรียนซึ่งเป็นข้อ จำกัดTที่System.Enumจะไม่สามารถทำทุกสิ่งกับTคนที่อาจคาดหวังผู้เขียนของ C # คิดว่าพวกเขาอาจห้ามมันโดยสิ้นเชิง ฉันพิจารณาการตัดสินใจที่โชคร้ายเนื่องจาก C # เพิกเฉยต่อSystem.Enumข้อ จำกัดพิเศษใด ๆจึงเป็นไปได้ที่จะเขียนHasAnyFlags<T>(this T it, T other)วิธีการขยายที่เป็นคำสั่งที่มีขนาดเร็วกว่าEnum.HasFlag(Enum)และตรวจสอบข้อโต้แย้งชนิดใด
supercat

9
ฉันไม่คิดว่าฉันเคยมีโครงการที่ฉันไม่ได้อยู่ที่นี่ C # 6 คือน้ำตาลในประโยค 110% และนี่ไม่ได้ใช่หรือไม่ ตัดอึ
Michael Blackburn

214

C # ≥ 7.3

เริ่มต้นด้วย C # 7.3 (พร้อมใช้งานกับ Visual Studio 2017 ≥ v15.7) ตอนนี้รหัสนี้ใช้ได้อย่างสมบูรณ์:

public static TEnum Parse<TEnum>(string value)
    where TEnum : struct, Enum
{
 ...
}

C # ≤ 7.2

คุณสามารถมีคอมไพเลอร์จริงบังคับใช้ข้อ จำกัด enum โดยใช้การสืบทอดข้อ จำกัด รหัสต่อไปนี้ระบุทั้ง a classและstructข้อ จำกัด ในเวลาเดียวกัน:

public abstract class EnumClassUtils<TClass>
where TClass : class
{

    public static TEnum Parse<TEnum>(string value)
    where TEnum : struct, TClass
    {
        return (TEnum) Enum.Parse(typeof(TEnum), value);
    }

}

public class EnumUtils : EnumClassUtils<Enum>
{
}

การใช้งาน:

EnumUtils.Parse<SomeEnum>("value");

หมายเหตุ: นี่ระบุไว้โดยเฉพาะในข้อกำหนดภาษา C # 5.0:

หากพารามิเตอร์ type S ขึ้นอยู่กับพารามิเตอร์ประเภท T ดังนั้น: [... ] จะใช้ได้สำหรับ S ที่จะมีข้อ จำกัด ประเภทค่าและ T เพื่อให้มีข้อ จำกัด ประเภทการอ้างอิง อย่างมีประสิทธิภาพนี้ จำกัด T กับประเภท System.Object, System.ValueType, System.Enum และประเภทอินเตอร์เฟสใด ๆ


7
@ DavidI.McIntosh EnumClassUtils<System.Enum>เพียงพอที่จะ จำกัด T ให้กับประเภทใด ๆSystem.Enumและที่ได้รับ structบนParseแล้ว จำกัด มันต่อไปจะเป็นชนิด enum จริง คุณจำเป็นต้อง จำกัดEnumบางจุด ในการทำเช่นนั้นคลาสของคุณจะต้องซ้อนกัน ดูgist.github.com/MrJul/7da12f5f2d6c69f03d79
Julien Lebosquain

7
เพียงเพื่อความชัดเจนความคิดเห็นของฉัน "ไม่พอใจ" ไม่ใช่ความคิดเห็นเกี่ยวกับโซลูชันของคุณ - เป็นแฮ็คที่สวยงามจริงๆ เพียงแค่ "ไม่พอใจ" ที่ MS บังคับให้เราใช้แฮ็คที่ซับซ้อนเช่นนี้
David I. McIntosh

2
มีวิธีการทำงานนี้เพื่อใช้สำหรับวิธีการขยายหรือไม่
Mord Zuber

3
อะไรwhere TClass : classกำไรข้อ จำกัด ที่นี่?
tsemer

2
@Trinkyoenum DefinitelyNotAnInt : byte { Realize, That, I, Am, Not, An, Int } enum AlsoNotAnInt : long { Well, Bummer }
M.Stramm

30

แก้ไข

คำถามที่ได้รับตอนนี้ยอดเยี่ยมตอบโดยJulien Lebosquain ฉันยังอยากจะขอคำตอบของเขาด้วยignoreCase, defaultValueและข้อโต้แย้งไม่จำเป็นขณะที่การเพิ่มและTryParseParseOrDefault

public abstract class ConstrainedEnumParser<TClass> where TClass : class
// value type constraint S ("TEnum") depends on reference type T ("TClass") [and on struct]
{
    // internal constructor, to prevent this class from being inherited outside this code
    internal ConstrainedEnumParser() {}
    // Parse using pragmatic/adhoc hard cast:
    //  - struct + class = enum
    //  - 'guaranteed' call from derived <System.Enum>-constrained type EnumUtils
    public static TEnum Parse<TEnum>(string value, bool ignoreCase = false) where TEnum : struct, TClass
    {
        return (TEnum)Enum.Parse(typeof(TEnum), value, ignoreCase);
    }
    public static bool TryParse<TEnum>(string value, out TEnum result, bool ignoreCase = false, TEnum defaultValue = default(TEnum)) where TEnum : struct, TClass // value type constraint S depending on T
    {
        var didParse = Enum.TryParse(value, ignoreCase, out result);
        if (didParse == false)
        {
            result = defaultValue;
        }
        return didParse;
    }
    public static TEnum ParseOrDefault<TEnum>(string value, bool ignoreCase = false, TEnum defaultValue = default(TEnum)) where TEnum : struct, TClass // value type constraint S depending on T
    {
        if (string.IsNullOrEmpty(value)) { return defaultValue; }
        TEnum result;
        if (Enum.TryParse(value, ignoreCase, out result)) { return result; }
        return defaultValue;
    }
}

public class EnumUtils: ConstrainedEnumParser<System.Enum>
// reference type constraint to any <System.Enum>
{
    // call to parse will then contain constraint to specific <System.Enum>-class
}

ตัวอย่างการใช้งาน:

WeekDay parsedDayOrArgumentException = EnumUtils.Parse<WeekDay>("monday", ignoreCase:true);
WeekDay parsedDayOrDefault;
bool didParse = EnumUtils.TryParse<WeekDay>("clubs", out parsedDayOrDefault, ignoreCase:true);
parsedDayOrDefault = EnumUtils.ParseOrDefault<WeekDay>("friday", ignoreCase:true, defaultValue:WeekDay.Sunday);

เก่า

การปรับปรุงเก่าของฉันในคำตอบของ Vivekโดยใช้ความคิดเห็นและการพัฒนา 'ใหม่':

  • ใช้TEnumเพื่อความชัดเจนสำหรับผู้ใช้
  • เพิ่มอินเตอร์เฟสที่มีข้อ จำกัด เพิ่มเติมสำหรับการตรวจสอบข้อ จำกัด เพิ่มเติม
  • ให้TryParseจัดการignoreCaseกับพารามิเตอร์ที่มีอยู่ (แนะนำใน VS2010 / .Net 4)
  • เลือกใช้defaultค่าทั่วไป(แนะนำใน VS2005 / .Net 2)
  • ใช้อาร์กิวเมนต์ที่เป็นตัวเลือก (แนะนำใน VS2010 / .Net 4) ด้วยค่าเริ่มต้นสำหรับdefaultValueและignoreCase

ที่เกิดขึ้นใน:

public static class EnumUtils
{
    public static TEnum ParseEnum<TEnum>(this string value,
                                         bool ignoreCase = true,
                                         TEnum defaultValue = default(TEnum))
        where TEnum : struct,  IComparable, IFormattable, IConvertible
    {
        if ( ! typeof(TEnum).IsEnum) { throw new ArgumentException("TEnum must be an enumerated type"); }
        if (string.IsNullOrEmpty(value)) { return defaultValue; }
        TEnum lResult;
        if (Enum.TryParse(value, ignoreCase, out lResult)) { return lResult; }
        return defaultValue;
    }
}

18

คุณสามารถกำหนด constructor แบบสแตติกสำหรับคลาสที่จะตรวจสอบว่า type T เป็น enum และโยนข้อยกเว้นถ้าไม่ใช่ นี่เป็นวิธีการที่ Jeffery Richter พูดถึงในหนังสือ CLR ผ่าน C #

internal sealed class GenericTypeThatRequiresAnEnum<T> {
    static GenericTypeThatRequiresAnEnum() {
        if (!typeof(T).IsEnum) {
        throw new ArgumentException("T must be an enumerated type");
        }
    }
}

จากนั้นในวิธีการวิเคราะห์คำคุณสามารถใช้ Enum.Parse (typeof (T), input, true) เพื่อแปลงจากสตริงเป็น enum พารามิเตอร์ true สุดท้ายใช้สำหรับละเว้นขนาดตัวพิมพ์ของอินพุต


1
นี่เป็นตัวเลือกที่ดีสำหรับคลาสทั่วไป - แต่แน่นอนว่ามันไม่ได้ช่วยวิธีการทั่วไป
McGarnagle

นอกจากนี้ยังไม่ได้มีการบังคับใช้ในเวลารวบรวมคุณจะรู้ว่าคุณได้ให้ข้อมูลที่ไม่ใช่Enum Tเมื่อตัวสร้างดำเนินการ แม้ว่าจะดีกว่าการรอคอยอินสแตนซ์คอนสตรัคเตอร์
jrh

15

ควรพิจารณาด้วยว่าตั้งแต่การเปิดตัว C # 7.3 โดยใช้ข้อ จำกัด Enum ได้รับการสนับสนุนนอกกรอบโดยไม่ต้องทำการตรวจสอบเพิ่มเติมและสิ่งของเพิ่มเติม

ดังนั้นไปข้างหน้าและให้คุณเปลี่ยนรุ่นภาษาของโครงการเป็น C # 7.3 รหัสต่อไปนี้จะทำงานได้อย่างสมบูรณ์แบบ:

    private static T GetEnumFromString<T>(string value, T defaultValue) where T : Enum
    {
        // Your code goes here...
    }

ในกรณีที่คุณไม่รู้วิธีเปลี่ยนเวอร์ชันภาษาเป็น C # 7.3 ให้ดูภาพหน้าจอต่อไปนี้: ป้อนคำอธิบายรูปภาพที่นี่

แก้ไข 1 - ต้องใช้ Visual Studio Version และกำลังพิจารณา ReSharper

เพื่อให้ Visual Studio รับรู้ไวยากรณ์ใหม่คุณต้องมีเวอร์ชัน 15.7 เป็นอย่างน้อย คุณจะพบว่ายังมีการกล่าวถึงในไมโครซอฟท์เปิดตัวโน้ตดูVisual Studio 2017 15.7 บันทึกประจำรุ่น ขอบคุณ @MohamedElshawaf สำหรับการชี้ให้เห็นคำถามที่ถูกต้องนี้

โปรดทราบว่าในกรณีของฉัน ReSharper 2018.1 ขณะที่เขียนแก้ไขนี้ยังไม่รองรับ C # 7.3 มี ReSharper เปิดใช้งานไฮไลท์ จำกัด Enum เป็นข้อผิดพลาดที่บอกผมไม่สามารถใช้ 'System.Array', 'System.Delegate', 'System.Enum', 'System.ValueType', 'วัตถุ' เป็นประเภท จำกัด ReSharper แนะนำว่าเป็นการแก้ไขอย่างรวดเร็วเพื่อลบข้อ จำกัด 'Enum' ประเภท paramter T ของวิธีการ

อย่างไรก็ตามหากคุณปิด ReSharper ชั่วคราวภายใต้เครื่องมือ -> ตัวเลือก -> ReSharper Ultimate -> ทั่วไปคุณจะเห็นว่าไวยากรณ์นั้นสมบูรณ์ดีเนื่องจากคุณใช้ VS 15.7 หรือสูงกว่าและ C # 7.3 หรือสูงกว่า


1
คุณใช้ VS เวอร์ชันใด
mshwf

1
@MohamedElshawaf ฉันเชื่อว่าเป็นรุ่น 15.7 ที่มีการรองรับ C # 7.3
Patrick Roberts

1
ฉันคิดว่ามันจะดีกว่าที่จะเขียนwhere T : struct, Enumเพื่อหลีกเลี่ยงการผ่านSystem.Enumตัวเองเป็นพารามิเตอร์ประเภท
Mariusz Pawelski

struct, Enumเช่นเดียวกับที่ผมเขียน @MariuszPawelski เหตุผลของฉันคือการอธิบายในคำตอบและความคิดเห็นที่นี่
สตีเฟ่นเคนเนดี

ข้อมูล ReSharper ช่วยฉันได้จริงๆ หมายเหตุรุ่นตัวอย่างล่าสุดรองรับคุณสมบัตินี้
DalSoft

11

ฉันแก้ไขตัวอย่างโดย dimarzionist รุ่นนี้จะใช้งานได้กับ Enums เท่านั้นและจะไม่ปล่อยให้ structs ผ่านไปได้

public static T ParseEnum<T>(string enumString)
    where T : struct // enum 
    {
    if (String.IsNullOrEmpty(enumString) || !typeof(T).IsEnum)
       throw new Exception("Type given must be an Enum");
    try
    {

       return (T)Enum.Parse(typeof(T), enumString, true);
    }
    catch (Exception ex)
    {
       return default(T);
    }
}

13
ฉันจะไม่ส่งคืนค่าเริ่มต้นเมื่อเกิดความล้มเหลว ฉันจะให้ข้อยกเว้นเผยแพร่ (เช่นเดียวกับ Enum.Parse) ให้ใช้ TryParse ส่งคืนบูลและส่งคืนผลลัพธ์โดยใช้พารามิเตอร์
Mark Simpson

1
OP ต้องการให้เป็นแบบตรงตามตัวพิมพ์ใหญ่ - เล็กนี่ไม่ใช่
Konrad Morawski

9

ฉันพยายามปรับปรุงรหัสเล็กน้อย:

public T LoadEnum<T>(string value, T defaultValue = default(T)) where T : struct, IComparable, IFormattable, IConvertible
{
    if (Enum.IsDefined(typeof(T), value))
    {
        return (T)Enum.Parse(typeof(T), value, true);
    }
    return defaultValue;
}

1
นี่เป็นคำตอบที่ดีกว่าคำตอบที่ยอมรับได้เพราะมันช่วยให้คุณสามารถโทรdefaultValue.ToString("D", System.Globalization.NumberFormatInfo.CurrentInfo)ถึงแม้ว่าคุณจะไม่ทราบว่ามันคือประเภทของ enum เพียงว่าวัตถุนั้นเป็น enum
styfle

1
การตรวจสอบล่วงหน้าด้วยIsDefinedจะทำลายความรู้สึกตัวพิมพ์เล็กและตัวพิมพ์ใหญ่ ซึ่งแตกต่างParse, IsDefinedไม่เคยมีใครignoreCaseโต้แย้งและ MSDN กล่าวว่ามันตรงกับกรณีที่แน่นอน
Nyerguds

5

ฉันมีข้อกำหนดเฉพาะที่ฉันต้องการใช้ enum กับข้อความที่เกี่ยวข้องกับค่า enum ตัวอย่างเช่นเมื่อฉันใช้ enum เพื่อระบุประเภทข้อผิดพลาดที่จำเป็นในการอธิบายรายละเอียดข้อผิดพลาด

public static class XmlEnumExtension
{
    public static string ReadXmlEnumAttribute(this Enum value)
    {
        if (value == null) throw new ArgumentNullException("value");
        var attribs = (XmlEnumAttribute[]) value.GetType().GetField(value.ToString()).GetCustomAttributes(typeof (XmlEnumAttribute), true);
        return attribs.Length > 0 ? attribs[0].Name : value.ToString();
    }

    public static T ParseXmlEnumAttribute<T>(this string str)
    {
        foreach (T item in Enum.GetValues(typeof(T)))
        {
            var attribs = (XmlEnumAttribute[])item.GetType().GetField(item.ToString()).GetCustomAttributes(typeof(XmlEnumAttribute), true);
            if(attribs.Length > 0 && attribs[0].Name.Equals(str)) return item;
        }
        return (T)Enum.Parse(typeof(T), str, true);
    }
}

public enum MyEnum
{
    [XmlEnum("First Value")]
    One,
    [XmlEnum("Second Value")]
    Two,
    Three
}

 static void Main()
 {
    // Parsing from XmlEnum attribute
    var str = "Second Value";
    var me = str.ParseXmlEnumAttribute<MyEnum>();
    System.Console.WriteLine(me.ReadXmlEnumAttribute());
    // Parsing without XmlEnum
    str = "Three";
    me = str.ParseXmlEnumAttribute<MyEnum>();
    System.Console.WriteLine(me.ReadXmlEnumAttribute());
    me = MyEnum.One;
    System.Console.WriteLine(me.ReadXmlEnumAttribute());
}

4

หวังว่านี่จะเป็นประโยชน์:

public static TValue ParseEnum<TValue>(string value, TValue defaultValue)
                  where TValue : struct // enum 
{
      try
      {
            if (String.IsNullOrEmpty(value))
                  return defaultValue;
            return (TValue)Enum.Parse(typeof (TValue), value);
      }
      catch(Exception ex)
      {
            return defaultValue;
      }
}

1
หากคุณต้องการความรู้สึกตัวพิมพ์เล็กและใหญ่ให้แทนที่return (TValue)Enum.Parse(typeof (TValue), value);โดยreturn (TValue)Enum.Parse(typeof (TValue), value, true);
เปาโลซานโตส

3

น่าสนใจพอเห็นได้ชัดว่าเป็นไปได้ใน langauges อื่น ๆ (Managed C ++, IL โดยตรง)

อ้างจาก:

... ข้อ จำกัด ทั้งสองผลิตจริง IL ที่ถูกต้องและยังสามารถบริโภคโดย C # ถ้าเขียนในภาษาอื่น (คุณสามารถประกาศข้อ จำกัด เหล่านั้นในการจัดการ C ++ หรือใน IL)

ใครจะรู้


2
ส่วนขยายที่ได้รับการจัดการสำหรับ C ++ ไม่มีการสนับสนุนใด ๆ สำหรับ generics ฉันคิดว่าคุณหมายถึง C ++ / CLI
Ben Voigt

3

นี่คือสิ่งที่ฉันทำ รวมจากคำตอบและ MSDN

public static TEnum ParseToEnum<TEnum>(this string text) where TEnum : struct, IConvertible, IComparable, IFormattable
{
    if (string.IsNullOrEmpty(text) || !typeof(TEnum).IsEnum)
        throw new ArgumentException("TEnum must be an Enum type");

    try
    {
        var enumValue = (TEnum)Enum.Parse(typeof(TEnum), text.Trim(), true);
        return enumValue;
    }
    catch (Exception)
    {
        throw new ArgumentException(string.Format("{0} is not a member of the {1} enumeration.", text, typeof(TEnum).Name));
    }
}

ที่มา MSDN


2
มันไม่สมเหตุสมผลเลย หากTEnumเป็นประเภท Enum จริง ๆ แต่textเป็นสตริงว่างคุณจะได้รับข้อความArgumentExceptionว่า "TEnum ต้องเป็นประเภท Enum" แม้ว่าจะเป็น
นิค

3

คำตอบที่มีอยู่นั้นเป็นจริงตั้งแต่ C # <= 7.2 อย่างไรก็ตามมีคำขอคุณสมบัติภาษา C # (เชื่อมโยงกับคำขอคุณสมบัติcorefx ) เพื่ออนุญาตสิ่งต่อไปนี้

public class MyGeneric<TEnum> where TEnum : System.Enum
{ }

ในช่วงเวลาของการเขียนคุณลักษณะคือ "ในการสนทนา" ที่การประชุมการพัฒนาภาษา

แก้ไข

เป็นต่อNawfalข้อมูล 's นี้จะถูกนำมาใช้ใน C # 7.3


1
การอภิปรายที่น่าสนใจที่นั่นขอบคุณ ยังไม่มีการตั้งค่าในหิน แต่ (ยัง)
johnc

1
@ Johnc เป็นเรื่องจริง แต่ก็คุ้มค่ากับการบันทึกและเป็นคุณลักษณะที่ถามบ่อย อัตราต่อรองที่เป็นธรรมกับมันเข้ามา
DiskJunky

1
นี้จะมาใน C # 7.3: docs.microsoft.com/en-us/visualstudio/releasenotes/... :)
nawfal

1

ฉันชอบสิ่งนี้เสมอ (คุณสามารถแก้ไขได้ตามความเหมาะสม):

public static IEnumerable<TEnum> GetEnumValues()
{
  Type enumType = typeof(TEnum);

  if(!enumType.IsEnum)
    throw new ArgumentException("Type argument must be Enum type");

  Array enumValues = Enum.GetValues(enumType);
  return enumValues.Cast<TEnum>();
}

1

ฉันชอบโซลูชันของ Christopher Currens ที่ใช้ IL แต่สำหรับผู้ที่ไม่ต้องการจัดการกับธุรกิจที่ยุ่งยากรวมถึง MSIL ในกระบวนการสร้างของพวกเขาฉันได้เขียนฟังก์ชันที่คล้ายกันใน C #

โปรดทราบว่าคุณไม่สามารถใช้ข้อ จำกัด ทั่วไปเช่นwhere T : Enumเนื่องจาก Enum เป็นประเภทพิเศษ ดังนั้นฉันต้องตรวจสอบว่าประเภททั่วไปที่กำหนดเป็นจริงหรือไม่

ฟังก์ชั่นของฉันคือ

public static T GetEnumFromString<T>(string strValue, T defaultValue)
{
    // Check if it realy enum at runtime 
    if (!typeof(T).IsEnum)
        throw new ArgumentException("Method GetEnumFromString can be used with enums only");

    if (!string.IsNullOrEmpty(strValue))
    {
        IEnumerator enumerator = Enum.GetValues(typeof(T)).GetEnumerator();
        while (enumerator.MoveNext())
        {
            T temp = (T)enumerator.Current;
            if (temp.ToString().ToLower().Equals(strValue.Trim().ToLower()))
                return temp;
        }
    }

    return defaultValue;
}

1

ฉันได้หุ้มโซลูชันของ Vivek ไว้ในคลาสยูทิลิตี้ที่คุณสามารถใช้ซ้ำ โปรดทราบว่าคุณยังควรกำหนดข้อ จำกัด ประเภท "โดยที่ T: struct, IConvertible" ในประเภทของคุณ

using System;

internal static class EnumEnforcer
{
    /// <summary>
    /// Makes sure that generic input parameter is of an enumerated type.
    /// </summary>
    /// <typeparam name="T">Type that should be checked.</typeparam>
    /// <param name="typeParameterName">Name of the type parameter.</param>
    /// <param name="methodName">Name of the method which accepted the parameter.</param>
    public static void EnforceIsEnum<T>(string typeParameterName, string methodName)
        where T : struct, IConvertible
    {
        if (!typeof(T).IsEnum)
        {
            string message = string.Format(
                "Generic parameter {0} in {1} method forces an enumerated type. Make sure your type parameter {0} is an enum.",
                typeParameterName,
                methodName);

            throw new ArgumentException(message);
        }
    }

    /// <summary>
    /// Makes sure that generic input parameter is of an enumerated type.
    /// </summary>
    /// <typeparam name="T">Type that should be checked.</typeparam>
    /// <param name="typeParameterName">Name of the type parameter.</param>
    /// <param name="methodName">Name of the method which accepted the parameter.</param>
    /// <param name="inputParameterName">Name of the input parameter of this page.</param>
    public static void EnforceIsEnum<T>(string typeParameterName, string methodName, string inputParameterName)
        where T : struct, IConvertible
    {
        if (!typeof(T).IsEnum)
        {
            string message = string.Format(
                "Generic parameter {0} in {1} method forces an enumerated type. Make sure your input parameter {2} is of correct type.",
                typeParameterName,
                methodName,
                inputParameterName);

            throw new ArgumentException(message);
        }
    }

    /// <summary>
    /// Makes sure that generic input parameter is of an enumerated type.
    /// </summary>
    /// <typeparam name="T">Type that should be checked.</typeparam>
    /// <param name="exceptionMessage">Message to show in case T is not an enum.</param>
    public static void EnforceIsEnum<T>(string exceptionMessage)
        where T : struct, IConvertible
    {
        if (!typeof(T).IsEnum)
        {
            throw new ArgumentException(exceptionMessage);
        }
    }
}

1

ฉันสร้างส่วนขยายวิธีto get integer value from enum ดูการใช้งานวิธี

public static int ToInt<T>(this T soure) where T : IConvertible//enum
{
    if (typeof(T).IsEnum)
    {
        return (int) (IConvertible)soure;// the tricky part
    }
    //else
    //    throw new ArgumentException("T must be an enumerated type");
    return soure.ToInt32(CultureInfo.CurrentCulture);
}

นี่คือการใช้งาน

MemberStatusEnum.Activated.ToInt()// using extension Method
(int) MemberStatusEnum.Activated //the ordinary way

แม้ว่ามันอาจจะใช้ได้ แต่ก็แทบจะไม่เกี่ยวข้องกับคำถาม
quetzalcoatl

1

ตามที่ระบุไว้ในคำตอบอื่น ๆ ก่อน; ขณะนี้ไม่สามารถแสดงในซอร์สโค้ดมันสามารถทำได้จริงในระดับ IL @Christopher Currens ตอบแสดงให้เห็นว่า IL ทำเช่นไร

ด้วยExtraConstraints Add-In ของFody เพราะมีวิธีที่ง่ายมากพร้อมด้วยเครื่องมือสร้างเพื่อให้บรรลุสิ่งนี้ เพียงแค่เพิ่มแพ็คเกจ nuget ( Fody, ExtraConstraints.Fody) ลงในโครงการของคุณและเพิ่มข้อ จำกัด ดังต่อไปนี้ (ข้อความที่ตัดตอนมาจาก Readme of ExtraConstraints):

public void MethodWithEnumConstraint<[EnumConstraint] T>() {...}

public void MethodWithTypeEnumConstraint<[EnumConstraint(typeof(ConsoleColor))] T>() {...}

และ Fody จะเพิ่ม IL ที่จำเป็นเพื่อให้มีข้อ จำกัด บันทึกคุณสมบัติเพิ่มเติมของการ จำกัด ผู้ร่วมประชุมด้วย:

public void MethodWithDelegateConstraint<[DelegateConstraint] T> ()
{...}

public void MethodWithTypeDelegateConstraint<[DelegateConstraint(typeof(Func<int>))] T> ()
{...}

เกี่ยวกับ Enums คุณอาจต้องการที่จะทราบที่น่าสนใจอย่างมากEnums.NET


1

นี่คือการดำเนินการของฉัน โดยทั่วไปคุณสามารถตั้งค่าคุณลักษณะใดก็ได้และใช้งานได้

public static class EnumExtensions
    {
        public static string GetDescription(this Enum @enum)
        {
            Type type = @enum.GetType();
            FieldInfo fi = type.GetField(@enum.ToString());
            DescriptionAttribute[] attrs =
                fi.GetCustomAttributes(typeof(DescriptionAttribute), false) as DescriptionAttribute[];
            if (attrs.Length > 0)
            {
                return attrs[0].Description;
            }
            return null;
        }
    }

0

ถ้าไม่ใช้การคัดเลือกนักแสดงโดยตรงหลังจากนั้นฉันคิดว่าคุณสามารถใช้System.Enumคลาสฐานในวิธีการของคุณได้ทุกที่ที่จำเป็น คุณเพียงแค่ต้องเปลี่ยนพารามิเตอร์ประเภทอย่างระมัดระวัง ดังนั้นวิธีการใช้งานจะเป็นเช่น:

public static class EnumUtils
{
    public static Enum GetEnumFromString(string value, Enum defaultValue)
    {
        if (string.IsNullOrEmpty(value)) return defaultValue;
        foreach (Enum item in Enum.GetValues(defaultValue.GetType()))
        {
            if (item.ToString().ToLower().Equals(value.Trim().ToLower())) return item;
        }
        return defaultValue;
    }
}

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

var parsedOutput = (YourEnum)EnumUtils.GetEnumFromString(someString, YourEnum.DefaultValue);

การใช้Enum.ToObject()จะให้ผลลัพธ์ที่ยืดหยุ่นมากขึ้น เพิ่มไปที่คุณสามารถทำการเปรียบเทียบสตริงโดยไม่ต้องคำนึงถึงตัวพิมพ์เล็กและตัวพิมพ์ใหญ่ซึ่งจะลบล้างความต้องการโทรToLower()
DiskJunky

-6

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

ปัญหาคือไม่มีวิธีใดที่จะทราบว่าการแจงนับใดที่อาจตรงกัน - ดังนั้นคำตอบคือการแก้ปัญหานั้น

แทนที่จะยอมรับเฉพาะค่าสตริงให้ยอมรับสตริงที่มีทั้งการแจงนับและค่าในรูปแบบ "enumeration.value" รหัสการทำงานอยู่ด้านล่าง - ต้องใช้ Java 1.8 หรือใหม่กว่า สิ่งนี้จะทำให้ XML มีความแม่นยำมากขึ้นในขณะที่คุณเห็นบางสิ่งบางอย่างเช่น color = "Color.red" แทนที่จะเป็นเพียง color = "red"

คุณจะเรียกเมธอด acceptEnumeratedValue () ด้วยสตริงที่มีชื่อค่าชื่อจุด enum

วิธีการส่งกลับค่าที่ระบุอย่างเป็นทางการ

import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;


public class EnumFromString {

    enum NumberEnum {One, Two, Three};
    enum LetterEnum {A, B, C};


    Map<String, Function<String, ? extends Enum>> enumsByName = new HashMap<>();

    public static void main(String[] args) {
        EnumFromString efs = new EnumFromString();

        System.out.print("\nFirst string is NumberEnum.Two - enum is " + efs.acceptEnumeratedValue("NumberEnum.Two").name());
        System.out.print("\nSecond string is LetterEnum.B - enum is " + efs.acceptEnumeratedValue("LetterEnum.B").name());

    }

    public EnumFromString() {
        enumsByName.put("NumberEnum", s -> {return NumberEnum.valueOf(s);});
        enumsByName.put("LetterEnum", s -> {return LetterEnum.valueOf(s);});
    }

    public Enum acceptEnumeratedValue(String enumDotValue) {

        int pos = enumDotValue.indexOf(".");

        String enumName = enumDotValue.substring(0, pos);
        String value = enumDotValue.substring(pos + 1);

        Enum enumeratedValue = enumsByName.get(enumName).apply(value);

        return enumeratedValue;
    }


}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.