枚举类型 "Inheritance"

475

我在一个低层命名空间中有一个枚举。我想在一个中级命名空间中提供一个类或枚举,该类或枚举“继承”低级别枚举。

namespace low
{
   public enum base
   {
      x, y, z
   }
}

namespace mid
{
   public enum consume : low.base
   {
   }
}

我希望这是可能的,或者有一种类可以替代枚举“consume”,为枚举提供一层抽象,但仍然让该类的实例访问枚举。

你有什么想法吗?

编辑: 我没有将其切换为类中的常量的其中一个原因是,必须使用低级别的枚举来消耗一个服务。我已经获得了WSDL和XSD,它们将结构定义为枚举。该服务不能更改。


1
一个选项是 https://www.codeproject.com/Articles/20805/Enhancing-C-Enums - Walter Verhoeven
1
你可以使用Int32,并根据需要将其强制转换为枚举类型。 - Dan Csharpster
你可以使用代码脚本。阅读原始代码并创建新代码。我有工作流和工作流过滤器。它们除了一个ALL值之外是相同的。我只需使用脚本来创建具有所有值的新工作流过滤器。请参见T4。虽然不在语言中,但总有办法。 - KenF
17个回答

534

这是不可能的。枚举类型不能继承其他枚举类型。实际上,所有枚举类型都必须继承自System.Enum。C#允许使用语法更改枚举值的基础表示,看起来像是继承,但实际上它们仍然继承自System.enum。

请参见CLI 规范第8.5.2节以获取完整详细信息。规范中相关信息如下:

  • 所有枚举类型都必须派生自System.Enum
  • 由于上述原因,所有枚举类型都是值类型,因此是sealed的

2
所有值类型都派生自System.ValueType。 - Rezo Megrelidze
4
必须提一下 @Seven 的回答是一个合理的解决方案:https://dev59.com/oHRA5IYBdhLWcg3w_DAw#4042826 - Tohid
但是@Steven的答案不能在switch情况下使用。 - zionpi
@zionpi 是的,但请注意(我相信)标准开关会被编译成与完整if、else if、else块相同的IL代码:我认为这种语法更好。你会失去Resharper/VS自动完成所有case语句的能力,但我认为这并不是世界末日。这是个人偏好,但我不喜欢switch语句。 - MemeDeveloper

178

你可以使用类来实现你想要的功能:

public class Base
{
    public const int A = 1;
    public const int B = 2;
    public const int C = 3;
}
public class Consume : Base
{
    public const int D = 4;
    public const int E = 5;
}

现在您可以像使用枚举一样使用这些类:

int i = Consume.B;

更新(在您更新问题之后):

如果您将常量定义为与现有枚举中定义的相同的 int 值,则可以在枚举和常量之间进行强制类型转换,例如:

public enum SomeEnum // this is the existing enum (from WSDL)
{
    A = 1,
    B = 2,
    ...
}
public class Base
{
    public const int A = (int)SomeEnum.A;
    //...
}
public class Consume : Base
{
    public const int D = 4;
    public const int E = 5;
}

// where you have to use the enum, use a cast:
SomeEnum e = (SomeEnum)Consume.B;

14
那么你如何枚举这个类中的字段呢?对我来说,这是一个枚举类型的重要行为:Enum.GetValues(typeof(MyEnum)) - Mike de Klerk
1
你可以使用反射:void Test() { foreach (System.Reflection.PropertyInfo pi in typeof(Consume).GetProperties()) { Console.WriteLine(pi.Name); } } - mnieto
2
你需要确保使用反射收集属性时忽略继承的Object属性。 - Robert
2
反射对于那个任务来说非常丑陋。 - dylanh724
2
不需要使用反射,可以使用https://www.codeproject.com/Articles/20805/Enhancing-C-Enums中的实现方法,因为在创建对象时,它们会被添加到列表中,该列表可用于返回对象类型列表。当与继承混合使用时,必须确保对于继承类使用正确的列表。 - PBo
显示剩余4条评论

136
简短的回答是不行的。如果你愿意,你可以玩一下:
你总是可以像这样做:
private enum Base
{
    A,
    B,
    C
}

private enum Consume
{
    A = Base.A,
    B = Base.B,
    C = Base.C,
    D,
    E
}

但是,这并不太好用,因为Base.A != Consume.A。

不过你可以尝试以下做法:

public static class Extensions
{
    public static T As<T>(this Consume c) where T : struct
    {
        return (T)System.Enum.Parse(typeof(T), c.ToString(), false);
    }
}

为了在Base和Consume之间进行转换,你可以使用枚举类型。但是,如果你想比较两个枚举值,你需要使用.Equals()方法而不是==运算符。你还可以将枚举值转换为整数并将它们作为整数进行比较,但那也不太好。
扩展方法的返回值应该进行类型转换,转换成类型T。

4
我明白了。翻译如下:老兄,我很欣赏这个想法。我用它将一些枚举类型从我的ORM传递到公共接口中(让没有引用ORM的人也能使用)。 - ObjectType
9
可以对枚举类型进行转换以进行比较: Base.A == (Base)Consume.A - Kresimir
1
使用(decimal)Base.A == (decimal)Consume.A。原因:这是位标志/掩码组合的工作方式(例如在Enum.IsDefined https://msdn.microsoft.com/en-us/library/system.enum.isdefined(v=vs.110).aspx中的示例)。因此,枚举可以设置为未在枚举中定义的整数。Consume test = 123456; - TamusJRoyce
4
@TamusJRoyce 说"十进制数?用 int 更有意义啊。枚举类型什么时候会有小数部分呢!?!?!" - ErikE
至少,这确保了相应的枚举常量具有相同的整数值。毕竟,枚举只是一组整数常量。C#不强制执行分配的值是有效的枚举常量,即在严格意义上,枚举不是类型安全的。 - Olivier Jacot-Descombes
显示剩余4条评论

111

上述使用带有int常量的类的解决方案缺乏类型安全性。也就是说,您实际上可以发明新值,这些新值在类中并未定义。 此外,例如无法编写一个以这些类作为输入的方法。

您需要编写:

public void DoSomethingMeaningFull(int consumeValue) ...

然而,在Java早期没有枚举可用的时候,有一种基于类的解决方案可以提供几乎相似的枚举行为。唯一需要注意的是,这些常量不能在switch语句中使用。

public class MyBaseEnum
{
    public static readonly MyBaseEnum A = new MyBaseEnum( 1 );
    public static readonly MyBaseEnum B = new MyBaseEnum( 2 );
    public static readonly MyBaseEnum C = new MyBaseEnum( 3 );

    public int InternalValue { get; protected set; }

    protected MyBaseEnum( int internalValue )
    {
        this.InternalValue = internalValue;
    }
}

public class MyEnum : MyBaseEnum
{
    public static readonly MyEnum D = new MyEnum( 4 );
    public static readonly MyEnum E = new MyEnum( 5 );

    protected MyEnum( int internalValue ) : base( internalValue )
    {
        // Nothing
    }
}

[TestMethod]
public void EnumTest()
{
    this.DoSomethingMeaningful( MyEnum.A );
}

private void DoSomethingMeaningful( MyBaseEnum enumValue )
{
    // ...
    if( enumValue == MyEnum.A ) { /* ... */ }
    else if (enumValue == MyEnum.B) { /* ... */ }
    // ...
}

7
我认为这是正确的答案。枚举类型无法继承,但这可以让你管理它! - robob
12
干净整洁。+1。只是一个提示,你真的不需要int值。 - Ignacio Soler Garcia
1
我从未将枚举视为FileOpenMode.Read > FileOpenMode.Write这样的比较。如果是这种情况,那么您需要一个int或者更好的IEqualityComparer来处理该枚举。祝好。 - Ignacio Soler Garcia
3
使用随机的“对象”会使得每个实例化的程序集的值都不同,因此无法进行序列化。 - ivan_pozdeev
2
但是这并不允许你在 MyEnum 上“切换”,对吧?我的意思是这就是我使用枚举的主要原因... - MemphiZ
显示剩余2条评论

15

忽略 base 是保留字这一事实,你不能继承枚举。

最好的做法是这样的:

public enum Baseenum
{
   x, y, z
}

public enum Consume
{
   x = Baseenum.x,
   y = Baseenum.y,
   z = Baseenum.z
}

public void Test()
{
   Baseenum a = Baseenum.x;
   Consume newA = (Consume) a;

   if ((Int32) a == (Int32) newA)
   {
   MessageBox.Show(newA.ToString());
   }
}

由于它们都是相同的基本类型(即int),您可以通过强制转换将一个类型的实例的值分配给另一个类型。这不是理想的,但它能够工作。


2
base是保留字,但Base不是。 - erikkallen
2
他指的是OP使用“base”作为枚举名称,这只是一个示例名称,我相信。 - John Rasch
与Genisio的答案相同。 - nawfal

11

这是我做的事情。我所做的不同之处在于,在“使用”enum时使用相同的名称和new关键字。由于enum的名称相同,您可以简单地无意识地使用它,它将是正确的。此外,您还可以获得Intellisense。您只需手动处理设置时从基础复制过来的值并保持它们同步即可。您可以通过代码注释帮助完成这项任务。这也是为什么在数据库中存储enum值时,我始终存储字符串而不是值的另一个原因。因为如果您正在使用自动分配的递增整数值,那么这些值随时间可能会发生变化。

// Base Class for balls 
public class Ball
{
    // keep synced with subclasses!
    public enum Sizes
    {
        Small,
        Medium,
        Large
    }
}

public class VolleyBall : Ball
{
    // keep synced with base class!
    public new enum Sizes
    {
        Small  = Ball.Sizes.Small,
        Medium = Ball.Sizes.Medium,
        Large  = Ball.Sizes.Large,
        SmallMedium,
        MediumLarge,
        Ginormous
    }
}

3
考虑为派生类中的新值(例如“SmallMedium = 100”)设置不同的范围,这样当您在基类中添加新值时,可以保持与旧版本软件的兼容性。例如,在基本枚举中添加一个“Huge”大小将为其分配值“4”,但是“SmallMedium”在派生类中已经使用了值“4”。 - Roberto
1
@Roberto,为了解决这个问题,我从不持久化枚举值,只持久化名称。并且保持它们同步是必要的。因此,在基类中添加“Huge”需要在“SmallMedium”之前在子类中添加“Huge”。 - toddmo

7

我知道这个答案有点晚,但这就是我最终做的事情:

public class BaseAnimal : IEquatable<BaseAnimal>
{
    public string Name { private set; get; }
    public int Value { private set; get; }

    public BaseAnimal(int value, String name)
    {
        this.Name = name;
        this.Value = value;
    }

    public override String ToString()
    {
        return Name;
    }

    public bool Equals(BaseAnimal other)
    {
        return other.Name == this.Name && other.Value == this.Value;
    }
}

public class AnimalType : BaseAnimal
{
    public static readonly BaseAnimal Invertebrate = new BaseAnimal(1, "Invertebrate");

    public static readonly BaseAnimal Amphibians = new BaseAnimal(2, "Amphibians");

    // etc        
}

public class DogType : AnimalType
{
    public static readonly BaseAnimal Golden_Retriever = new BaseAnimal(3, "Golden_Retriever");

    public static readonly BaseAnimal Great_Dane = new BaseAnimal(4, "Great_Dane");

    // etc        
}

然后我就可以做这样的事情:

public void SomeMethod()
{
    var a = AnimalType.Amphibians;
    var b = AnimalType.Amphibians;

    if (a == b)
    {
        // should be equal
    }

    // call method as
    Foo(a);

    // using ifs
    if (a == AnimalType.Amphibians)
    {
    }
    else if (a == AnimalType.Invertebrate)
    {
    }
    else if (a == DogType.Golden_Retriever)
    {
    }
    // etc          
}

public void Foo(BaseAnimal typeOfAnimal)
{
}

3
根据https://dev59.com/oHRA5IYBdhLWcg3w_DAw#VQAMoYgBc1ULPQZF0aYd,可以将魔法数字替换为对象。但是对于这种特殊情况,您可以利用现有生物学命名法的特征来保证唯一性,从而获得两全其美的效果。 - ivan_pozdeev
DogTypeAnimalTypeBaseAnimal继承,有什么特殊的原因吗?依据我的理解,它们可以被转化为静态类。 - Ivaylo Slavov

6

替代方案

在我们公司,我们避免“跳过项目”以进入非通用低级项目。例如,我们的演示/API层只能引用我们的域层,域层只能引用数据层。

然而,当演示层和域层都需要引用枚举时,这就成了一个问题。

以下是我们实施的解决方案(目前为止)。这是一个相当好的解决方案,对我们很有效。其他答案也都涉及到了这个方面。

基本原则是枚举无法继承 - 但类可以。所以...

// In the lower level project (or DLL)...
public abstract class BaseEnums
{
    public enum ImportanceType
    {
        None = 0,
        Success = 1,
        Warning = 2,
        Information = 3,
        Exclamation = 4
    }

    [Flags]
    public enum StatusType : Int32
    {
        None = 0,
        Pending = 1,
        Approved = 2,
        Canceled = 4,
        Accepted = (8 | Approved),
        Rejected = 16,
        Shipped = (32 | Accepted),
        Reconciled = (64 | Shipped)
    }

    public enum Conveyance
    {
        None = 0,
        Feet = 1,
        Automobile = 2,
        Bicycle = 3,
        Motorcycle = 4,
        TukTuk = 5,
        Horse = 6,
        Yak = 7,
        Segue = 8
    }

接着,在另一个更高级别的项目中“继承”这些枚举类型...

// Class in another project
public sealed class SubEnums: BaseEnums
{
   private SubEnums()
   {}
}

这样做有三个真正的优势:
  1. 枚举定义在两个项目中都是自动相同的-按照定义。
  2. 对枚举定义进行任何更改,第二个项目都会自动反映这些更改,而不需要对第二个类进行任何修改。
  3. 这些枚举基于相同的代码-因此可以轻松比较这些值(但需要注意某些细节)。
要在第一个项目中引用这些枚举,请使用类的前缀:BaseEnums.StatusType.Pending或将 "using static BaseEnums;" 语句添加到你的 usings 中。
然而,在处理继承类时,我无法让 “using static…” 方法起作用,因此所有对"继承枚举"的引用都需要加上类的前缀,例如 SubEnums.StatusType.Pending。如果有人想出了一种方式使 "using static" 方法可以在第二个项目中使用,请让我知道。
我相信这可以调整得更好-但实际上这个方法是可行的,我已经在工作项目中使用过。

2
在我看来,你所做的是对分层架构的误解。例如运输对象、接口等合同结构可以和应该在层之间共享。如果限制了这个,你只会束缚自己,在处理管道中有时会失去上下文信息,这些信息应该从上下文和默认值中在管道后面恢复。我见过很多人犯这种错误。现代框架如GRPC也建议在架构垂直方向上分享这些结构。 - Daniel Leiszen
1
这个解决方案回答了提出的问题,并且在枚举类型没有共享源的情况下工作。我不认为这是一个“错误”。 - Thomas Phaneuf
2
好的,这不是一个错误,而是一种反模式。你把共享结构定义看作是层,但它们实际上并不是。因此,你需要使用符号、前缀和后缀来区分它们。我总是尝试在我的应用程序中以不同的名称命名事物。如果我找不到合适的名称,我会重新分析并考虑是否真的需要它。我避免使用符号和约定。那只是额外需要学习和记忆的知识。我尽可能分享代码。这是消除冗余的方法。毕竟,我不是自己的敌人。 - Daniel Leiszen
从未说过这是最好的方法。我所说的是它回答了所提出的问题。有时候,鉴于特定的上下文,这就是软件能做的全部。在某些情况下,定义无法从共同来源共享 - 这个答案解决了这个问题。 - Thomas Phaneuf

3
我知道我来晚了,但这是我的两分钱。
我们都清楚,枚举继承不受框架支持。本帖中提出了一些非常有趣的解决方法,但没有一个感觉像我正在寻找的那样,所以我自己试着写了一个。
介绍:ObjectEnum
您可以在此处检查代码和文档:https://github.com/dimi3tron/ObjectEnum
包在这里:https://www.nuget.org/packages/ObjectEnum 或者只需安装它:Install-Package ObjectEnum 简而言之,ObjectEnum<TEnum>作为任何枚举的包装器。通过在子类中重写GetDefinedValues(),可以指定哪些枚举值对于该特定类是有效的。
已添加了许多运算符重载,使ObjectEnum<TEnum>实例的行为就像它是基础枚举的实例,记住定义值的限制。这意味着您可以将实例与int或枚举值轻松比较,从而在switch case或任何其他条件中使用它。
我想参考上面提到的github仓库获取示例和更多信息。
希望您会发现这很有用。如有任何进一步想法或意见,请在github上评论或打开问题。
以下是使用ObjectEnum<TEnum>的几个简短例子:
var sunday = new WorkDay(DayOfWeek.Sunday); //throws exception
var monday = new WorkDay(DayOfWeek.Monday); //works fine
var label = $"{monday} is day {(int)monday}." //produces: "Monday is day 1."
var mondayIsAlwaysMonday = monday == DayOfWeek.Monday; //true, sorry...

var friday = new WorkDay(DayOfWeek.Friday);

switch((DayOfWeek)friday){
    case DayOfWeek.Monday:
        //do something monday related
        break;
        /*...*/
    case DayOfWeek.Friday:
        //do something friday related
        break;
}

2

我还想重载枚举并创建了一个混合了此页面上 'Seven' 的答案此重复帖子中 'Merlyn Morgan-Graham' 的答案的方案,并进行了一些改进。
我方案的主要优点如下:

  • 底层 int 值的自动递增
  • 自动命名

这是一个开箱即用的方案,可以直接插入您的项目中。它是根据我的需求设计的,因此如果您不喜欢其中的某些部分,请将其替换为您自己的代码。

首先,有一个基类CEnum,所有自定义枚举都应该从它继承。它具有基本功能,类似于 .net 的Enum类型:

public class CEnum
{
  protected static readonly int msc_iUpdateNames  = int.MinValue;
  protected static int          ms_iAutoValue     = -1;
  protected static List<int>    ms_listiValue     = new List<int>();

  public int Value
  {
    get;
    protected set;
  }

  public string Name
  {
    get;
    protected set;
  }

  protected CEnum ()
  {
    CommonConstructor (-1);
  }

  protected CEnum (int i_iValue)
  {
    CommonConstructor (i_iValue);
  }

  public static string[] GetNames (IList<CEnum> i_listoValue)
  {
    if (i_listoValue == null)
      return null;
    string[] asName = new string[i_listoValue.Count];
    for (int ixCnt = 0; ixCnt < asName.Length; ixCnt++)
      asName[ixCnt] = i_listoValue[ixCnt]?.Name;
    return asName;
  }

  public static CEnum[] GetValues ()
  {
    return new CEnum[0];
  }

  protected virtual void CommonConstructor (int i_iValue)
  {
    if (i_iValue == msc_iUpdateNames)
    {
      UpdateNames (this.GetType ());
      return;
    }
    else if (i_iValue > ms_iAutoValue)
      ms_iAutoValue = i_iValue;
    else
      i_iValue = ++ms_iAutoValue;

    if (ms_listiValue.Contains (i_iValue))
      throw new ArgumentException ("duplicate value " + i_iValue.ToString ());
    Value = i_iValue;
    ms_listiValue.Add (i_iValue);
  }

  private static void UpdateNames (Type i_oType)
  {
    if (i_oType == null)
      return;
    FieldInfo[] aoFieldInfo = i_oType.GetFields (BindingFlags.Public | BindingFlags.Static);

    foreach (FieldInfo oFieldInfo in aoFieldInfo)
    {
      CEnum oEnumResult = oFieldInfo.GetValue (null) as CEnum;
      if (oEnumResult == null)
        continue;
      oEnumResult.Name = oFieldInfo.Name;
    }
  }
}

其次,这里有两个派生枚举类。所有派生类都需要一些基本方法才能按预期工作。它们总是相同的样板代码;我还没有找到一种将其外包到基类中的方法。第一层继承的代码与所有后续层级略有不同。

public class CEnumResult : CEnum
{
  private   static List<CEnumResult>  ms_listoValue = new List<CEnumResult>();

  public    static readonly CEnumResult Nothing         = new CEnumResult (  0);
  public    static readonly CEnumResult SUCCESS         = new CEnumResult (  1);
  public    static readonly CEnumResult UserAbort       = new CEnumResult ( 11);
  public    static readonly CEnumResult InProgress      = new CEnumResult (101);
  public    static readonly CEnumResult Pausing         = new CEnumResult (201);
  private   static readonly CEnumResult Dummy           = new CEnumResult (msc_iUpdateNames);

  protected CEnumResult () : base ()
  {
  }

  protected CEnumResult (int i_iValue) : base (i_iValue)
  {
  }

  protected override void CommonConstructor (int i_iValue)
  {
    base.CommonConstructor (i_iValue);

    if (i_iValue == msc_iUpdateNames)
      return;
    if (this.GetType () == System.Reflection.MethodBase.GetCurrentMethod ().DeclaringType)
      ms_listoValue.Add (this);
  }

  public static new CEnumResult[] GetValues ()
  {
    List<CEnumResult> listoValue = new List<CEnumResult> ();
    listoValue.AddRange (ms_listoValue);
    return listoValue.ToArray ();
  }
}

public class CEnumResultClassCommon : CEnumResult
{
  private   static List<CEnumResultClassCommon> ms_listoValue = new List<CEnumResultClassCommon>();

  public    static readonly CEnumResult Error_InternalProgramming           = new CEnumResultClassCommon (1000);

  public    static readonly CEnumResult Error_Initialization                = new CEnumResultClassCommon ();
  public    static readonly CEnumResult Error_ObjectNotInitialized          = new CEnumResultClassCommon ();
  public    static readonly CEnumResult Error_DLLMissing                    = new CEnumResultClassCommon ();
  // ... many more
  private   static readonly CEnumResult Dummy                               = new CEnumResultClassCommon (msc_iUpdateNames);

  protected CEnumResultClassCommon () : base ()
  {
  }

  protected CEnumResultClassCommon (int i_iValue) : base (i_iValue)
  {
  }

  protected override void CommonConstructor (int i_iValue)
  {
    base.CommonConstructor (i_iValue);

    if (i_iValue == msc_iUpdateNames)
      return;
    if (this.GetType () == System.Reflection.MethodBase.GetCurrentMethod ().DeclaringType)
      ms_listoValue.Add (this);
  }

  public static new CEnumResult[] GetValues ()
  {
    List<CEnumResult> listoValue = new List<CEnumResult> (CEnumResult.GetValues ());
    listoValue.AddRange (ms_listoValue);
    return listoValue.ToArray ();
  }
}

这些类已经成功地通过以下代码进行了测试:
private static void Main (string[] args)
{
  CEnumResult oEnumResult = CEnumResultClassCommon.Error_Initialization;
  string sName = oEnumResult.Name;   // sName = "Error_Initialization"

  CEnum[] aoEnumResult = CEnumResultClassCommon.GetValues ();   // aoEnumResult = {testCEnumResult.Program.CEnumResult[9]}
  string[] asEnumNames = CEnum.GetNames (aoEnumResult);
  int ixValue = Array.IndexOf (aoEnumResult, oEnumResult);    // ixValue = 6
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接