C#与Java枚举类型比较(适用于C#新手)

206

我在Java中编程已经有一段时间了,刚刚被安排到一个完全使用C#编写的项目中。我正在努力学习C#,并注意到我的新项目中在多个地方使用了枚举。但是乍一看,C#的枚举似乎比Java 1.5+实现更为简单。有人能够列举C#和Java枚举之间的区别,并说明如何克服这些差异吗?(我不想引发语言之争,我只想知道如何在C#中完成我以前在Java中所做的一些事情)。例如,有人可以发布一个C#版本的Sun著名的Planet枚举示例吗?

public enum Planet {
  MERCURY (3.303e+23, 2.4397e6),
  VENUS   (4.869e+24, 6.0518e6),
  EARTH   (5.976e+24, 6.37814e6),
  MARS    (6.421e+23, 3.3972e6),
  JUPITER (1.9e+27,   7.1492e7),
  SATURN  (5.688e+26, 6.0268e7),
  URANUS  (8.686e+25, 2.5559e7),
  NEPTUNE (1.024e+26, 2.4746e7),
  PLUTO   (1.27e+22,  1.137e6);

  private final double mass;   // in kilograms
  private final double radius; // in meters
  Planet(double mass, double radius) {
      this.mass = mass;
      this.radius = radius;
  }
  public double mass()   { return mass; }
  public double radius() { return radius; }

  // universal gravitational constant  (m3 kg-1 s-2)
  public static final double G = 6.67300E-11;

  public double surfaceGravity() {
      return G * mass / (radius * radius);
  }
  public double surfaceWeight(double otherMass) {
      return otherMass * surfaceGravity();
  }
}

// Example usage (slight modification of Sun's example):
public static void main(String[] args) {
    Planet pEarth = Planet.EARTH;
    double earthRadius = pEarth.radius(); // Just threw it in to show usage

    // Argument passed in is earth Weight.  Calculate weight on each planet:
    double earthWeight = Double.parseDouble(args[0]);
    double mass = earthWeight/pEarth.surfaceGravity();
    for (Planet p : Planet.values())
       System.out.printf("Your weight on %s is %f%n",
                         p, p.surfaceWeight(mass));
}

// Example output:
$ java Planet 175
Your weight on MERCURY is 66.107583
Your weight on VENUS is 158.374842
[etc ...]

1
@ycomp 我不能为此而自豪。这来自Sun(现在是Oracle):http://docs.oracle.com/javase/tutorial/java/javaOO/enum.html - Ogre Psalm33
13个回答

245

在C#中,你可以为枚举定义扩展方法,这弥补了一些缺失的功能。

你可以将Planet定义为枚举,并编写等效于surfaceGravity()surfaceWeight()的扩展方法。

我已经像Mikhail建议的那样使用自定义属性,但也可以使用Dictionary来实现同样的功能。

using System;
using System.Reflection;

class PlanetAttr: Attribute
{
    internal PlanetAttr(double mass, double radius)
    {
        this.Mass = mass;
        this.Radius = radius;
    }
    public double Mass { get; private set; }
    public double Radius { get; private set; }
}

public static class Planets
{
    public static double GetSurfaceGravity(this Planet p)
    {
        PlanetAttr attr = GetAttr(p);
        return G * attr.Mass / (attr.Radius * attr.Radius);
    }

    public static double GetSurfaceWeight(this Planet p, double otherMass)
    {
        return otherMass * p.GetSurfaceGravity();
    }

    public const double G = 6.67300E-11;

    private static PlanetAttr GetAttr(Planet p)
    {
        return (PlanetAttr)Attribute.GetCustomAttribute(ForValue(p), typeof(PlanetAttr));
    }

    private static MemberInfo ForValue(Planet p)
    {
        return typeof(Planet).GetField(Enum.GetName(typeof(Planet), p));
    }

}

public enum Planet
{
    [PlanetAttr(3.303e+23, 2.4397e6)]  MERCURY,
    [PlanetAttr(4.869e+24, 6.0518e6)]  VENUS,
    [PlanetAttr(5.976e+24, 6.37814e6)] EARTH,
    [PlanetAttr(6.421e+23, 3.3972e6)]  MARS,
    [PlanetAttr(1.9e+27,   7.1492e7)]  JUPITER,
    [PlanetAttr(5.688e+26, 6.0268e7)]  SATURN,
    [PlanetAttr(8.686e+25, 2.5559e7)]  URANUS,
    [PlanetAttr(1.024e+26, 2.4746e7)]  NEPTUNE,
    [PlanetAttr(1.27e+22,  1.137e6)]   PLUTO
}

26
我认为这应该得到更多的支持。它更接近Java中枚举的工作方式。我可以使用类似Planet.MERCURY.GetSurfaceGravity()的方式来执行操作,注意这是在枚举上的扩展方法! - user46915
5
绝对是的。在枚举上使用扩展方法(甚至是一般的扩展方法)是 C# 中非常好的补充。 - KeithS
4
@AllonGuralnek,谢谢。然而,并不是每个人都会同意元数据的使用。请参考MattDavey在a related codereview.stackexchange question中的评论。 - finnw
@finnw: 我从未听说过 Code Review SE 网站 - 多谢!我已经在那里继续了这个讨论(虽然它可能值得在 Programmers 上单独发一个问题)。 - Allon Guralnek
这是一个很好的解决方案 - 有人测试过它的性能吗?例如,与访问类的属性相比,访问属性需要多长时间。 - Simon Meyer
当然,反射调用是不切实际的——即使从性能角度来看使用了运行时缓存。这个答案得到高评价,因为它正确地提到了C#中最接近JAVA类似功能的__ENUM扩展方法__。 - Lorenz Lo Sauer

234

在CLR中,枚举只是具有名称的常量。其基础类型必须为整数。而在Java中,枚举更像是一个类型的命名实例。该类型可以相当复杂,并且 - 如您的示例所示 - 包含多个不同类型的字段。

要将示例移植到C#,我只需将枚举更改为不可变类,并公开该类的静态只读实例:

using System;
using System.Collections.Generic;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Planet planetEarth = Planet.MERCURY;

            double earthRadius = pEarth.Radius; // Just threw it in to show usage
            double earthWeight = double.Parse("123");
            double earthMass   = earthWeight / pEarth.SurfaceGravity();

            foreach (Planet p in Planet.Values)
                Console.WriteLine($"Your weight on {p} is {p.SurfaceWeight(mass)}");

            Console.ReadKey();
        }
    }

    public class Planet
    {
        public static readonly Planet MERCURY = new Planet("Mercury", 3.303e+23, 2.4397e6);
        public static readonly Planet VENUS   = new Planet("Venus", 4.869e+24, 6.0518e6);
        public static readonly Planet EARTH   = new Planet("Earth", 5.976e+24, 6.37814e6);
        public static readonly Planet MARS    = new Planet("Mars", 6.421e+23, 3.3972e6);
        public static readonly Planet JUPITER = new Planet("Jupiter", 1.9e+27, 7.1492e7);
        public static readonly Planet SATURN  = new Planet("Saturn", 5.688e+26, 6.0268e7);
        public static readonly Planet URANUS  = new Planet("Uranus", 8.686e+25, 2.5559e7);
        public static readonly Planet NEPTUNE = new Planet("Neptune", 1.024e+26, 2.4746e7);
        public static readonly Planet PLUTO   = new Planet("Pluto", 1.27e+22, 1.137e6);

        public static IEnumerable<Planet> Values
        {
            get
            {
                yield return MERCURY;
                yield return VENUS;
                yield return EARTH;
                yield return MARS;
                yield return JUPITER;
                yield return SATURN;
                yield return URANUS;
                yield return NEPTUNE;
                yield return PLUTO;
            }
        }

        public string Name   { get; private set; }
        public double Mass   { get; private set; }
        public double Radius { get; private set; }

        Planet(string name, double mass, double radius) => 
            (Name, Mass, Radius) = (name, mass, radius);

        // Wniversal gravitational constant  (m3 kg-1 s-2)
        public const double G = 6.67300E-11;
        public double SurfaceGravity()            => G * mass / (radius * radius);
        public double SurfaceWeight(double other) => other * SurfaceGravity();
        public override string ToString()         => name;
    }
}

9
像这种类型安全的枚举,我们这些被迫使用Java 1.4及以下版本的可怜人就得去实现它...Java 5的枚举可能是Java 5+中最好的特性,特别是因为它们可以在switch语句中使用。 - MetroidFan2002
9
@Chris:只有标记枚举应该使用复数形式。也就是说,枚举的成员使用 | 运算符组合的枚举类型。 - Kent Boogaart
5
@Mladen:这完全取决于上下文。在一个只提供有限行星数量的游戏中,行星的枚举可能非常适合。如果游戏中添加了新的行星,则对代码进行更改可能是您想要的。 - Kent Boogaart
25
哇...在C#中看到比Java更冗长的实现方式,这几乎令人难以置信。 - Sune Rasmussen
3
我觉得一开始也是这么认为的,但与 C# 不同,Java 中的 enum 可以为空。我认为在这个角度上,class 更加等同。 - Attacktive
显示剩余13条评论

40

1
不错!这种方法只有稍微有点笨拙,但是除此之外,它是一种非常可接受的向枚举添加额外数据的方法。我真的很惊讶,居然要等到现在才有人提出这个好方法! - Ogre Psalm33

13

1
虽然你提供的链接给出了C#和Java之间相似性和差异的有趣而广泛的概述,但是文本中有很多错误(例如错误地陈述了Java protected等于C# internal,而实际上应该是internal protected)。因此,不要把那里的所有内容都视为正确 :) - mafu
1
即使我是Java的粉丝,我也不会说Java枚举类型更优秀。C#支持简单的整数声明,例如FOO = 0,这在ORM工具中使用更加容易(无需使用易出错的ordinal())。此外,C#还支持位枚举类型,这通常非常有用,特别是与EntityFramework结合使用。Java应该扩展其枚举类型,以允许它们绑定到整数。那么它们就会更加优秀 :) - djmj

5
我认为应该是类似这样的内容:

就像这样:

public class Planets 
{
    public static readonly Planet MERCURY = new Planet(3.303e+23, 2.4397e6);
    public static readonly Planet VENUS = new Planet(4.869e+24, 6.0518e6);
    public static readonly Planet EARTH = new Planet(5.976e+24, 6.37814e6);
    public static readonly Planet MARS = new Planet(6.421e+23, 3.3972e6);
    public static readonly Planet JUPITER = new Planet(1.9e+27,   7.1492e7);
    public static readonly Planet SATURN = new Planet(5.688e+26, 6.0268e7);
    public static readonly Planet URANUS = new Planet(8.686e+25, 2.5559e7);
    public static readonly Planet NEPTUNE = new Planet(1.024e+26, 2.4746e7);
    public static readonly Planet PLUTO = new Planet(1.27e+22,  1.137e6);
}

public class Planet
{
    public double Mass {get;private set;}
    public double Radius {get;private set;}

    Planet(double mass, double radius)
    {
        Mass = mass;
        Radius = radius;
    }

    // universal gravitational constant  (m3 kg-1 s-2)
    private static readonly double G = 6.67300E-11;

    public double SurfaceGravity()
    {
        return G * Mass / (Radius * Radius);
    }

    public double SurfaceWeight(double otherMass)
    {
        return otherMass * SurfaceGravity();
    }
}

或者像上面一样将常量组合到Planet类中


9
有些不对- Planet 构造函数应该是私有的;枚举类型的一个重要特点是它们是一组固定的值。这些值应该在 Planet 类中进行定义。 - Jon Skeet
还没有。1)枚举器缺失 :) 2)枚举类型永远不应该是可变的。哦,最后,你的代码需要一个单一的类(特别是当你有一个私有构造函数时)。 - nawfal

5
我们刚刚为c#创建了一个枚举扩展https://github.com/simonmau/enum_ext。它只是typesafeenum的一种实现,但它运行得很好,所以我们制作了一个包来分享 - 玩得开心。
public sealed class Weekday : TypeSafeNameEnum<Weekday, int>
{
    public static readonly Weekday Monday = new Weekday(1, "--Monday--");
    public static readonly Weekday Tuesday = new Weekday(2, "--Tuesday--");
    public static readonly Weekday Wednesday = new Weekday(3, "--Wednesday--");
    ....

    private Weekday(int id, string name) : base(id, name)
    {
    }
}

4

这里有一个关于Java中可定制行为的有趣想法。我设计了以下Enumeration基类:

public abstract class Enumeration<T>
    where T : Enumeration<T>
{   
    protected static int nextOrdinal = 0;

    protected static readonly Dictionary<int, Enumeration<T>> byOrdinal = new Dictionary<int, Enumeration<T>>();
    protected static readonly Dictionary<string, Enumeration<T>> byName = new Dictionary<string, Enumeration<T>>();

    protected readonly string name;
    protected readonly int ordinal;

    protected Enumeration(string name)
        : this (name, nextOrdinal)
    {
    }

    protected Enumeration(string name, int ordinal)
    {
        this.name = name;
        this.ordinal = ordinal;
        nextOrdinal = ordinal + 1;
        byOrdinal.Add(ordinal, this);
        byName.Add(name, this);
    }

    public override string ToString()
    {
        return name;
    }

    public string Name 
    {
        get { return name; }
    }

    public static explicit operator int(Enumeration<T> obj)
    {
        return obj.ordinal;
    }

    public int Ordinal
    {
        get { return ordinal; }
    }
}

它基本上有一个类型参数,这样顺序计数在不同的衍生枚举中才能正常工作。Jon Skeet在其回答另一个问题时给出的Operator示例(https://dev59.com/iXM_5IYBdhLWcg3wcSt_

public class Operator : Enumeration<Operator>
{
    public static readonly Operator Plus = new Operator("Plus", (x, y) => x + y);
    public static readonly Operator Minus =  new Operator("Minus", (x, y) => x - y);
    public static readonly Operator Times =  new Operator("Times", (x, y) => x * y);
    public static readonly Operator Divide = new Operator("Divide", (x, y) => x / y);

    private readonly Func<int, int, int> op;

    // Prevent other top-level types from instantiating
    private Operator(string name, Func<int, int, int> op)
        :base (name)
    {
        this.op = op;
    }

    public int Execute(int left, int right)
    {
        return op(left, right);
    }
}

这样做有一些优点。

  • 可以支持序数
  • 可以转换为stringint,从而使 switch 语句可行
  • GetType()将为派生的枚举类型的每个值提供相同的结果。
  • System.Enum 中的静态方法可以添加到基础枚举类中,以便允许相同的功能。

2
我怀疑C#中的枚举只是CLR内部的常量,但对它们不太熟悉。我反编译了一些Java类,可以告诉您转换后什么是Enums。
Java做了一些聪明的事情。它将枚举类视为普通类,并在引用枚举值时使用大量宏。如果您在Java类中使用枚举的case语句,则将枚举引用替换为整数。如果您需要转换为字符串,则创建一个由序数索引的字符串数组,并在每个类中使用它。我怀疑这样做可以节省装箱开销。
如果您下载此反编译器,您将看到它如何创建并集成其类。说实话,这相当迷人。我过去不使用枚举类,因为我认为它对于仅包含常量的数组来说太臃肿了。我喜欢它比C#中有限的使用方式更好。 http://members.fortunecity.com/neshkov/dj.html -- Java反编译器

2
Java中的枚举是一种将枚举呈现为面向对象方式的语法糖。它们是Java中扩展Enum类的抽象类,每个枚举值就像是枚举类的静态final公共实例实现。查看生成的类,对于一个有10个值的枚举"Foo",你会看到生成了"Foo$1"到"Foo$10"类。
我不了解C#,但我可以猜测在该语言中,枚举更像是传统C风格语言中的枚举。从快速的谷歌搜索中可以看出,它们可以持有多个值,因此它们可能以类似的方式实现,但比Java编译器允许的限制要多得多。

3
Java和C#中的所有东西不都是为了JVM或CLR字节码而存在的语法糖吗? :) 只是这么说而已。 - user46915

2

Java枚举允许使用编译器生成的valueOf方法轻松进行类型安全转换,即通过名称转换为值。

// Java Enum has generics smarts and allows this
Planet p = Planet.valueOf("MERCURY");

在C#中,原始枚举的等效写法更加冗长:
// C# enum - bit of hoop jumping required
Planet p = (Planet)Enum.Parse(typeof(Planet), "MERCURY");

然而,如果你按照Kent建议的方法进行操作,你可以在枚举类中轻松实现ValueOf方法。


Java示例使用编译器生成的合成方法 - 完全与泛型无关。枚举确实具有通用的valueOf方法,但它使用Class的泛型而不是Enum的泛型。 - Tom Hawtin - tackline

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