如何在C#中获取下一个(或上一个)枚举值

71

我有一个枚举类型,定义如下:

public enum eRat { A = 0, B=3, C=5, D=8 };

假设给定值为eRat.B,我想获取下一个值eRat.C

我看到的解决方法是(不包括范围检查)

Array a = Enum.GetValues(typeof(eRat));
int i=0 ;
for (i = 0; i < a.GetLength(); i++)
{
       if (a.GetValue(i) == eRat.B)
            break;
}
return (eRat)a.GetValue(i+1):

现在这太过复杂了,对于这么简单的东西。你知道任何更好的解决方案吗?像eRat.B+1或者Enum.Next(Erat.B)这样的?

谢谢。


这取决于你。但是让我们假设A,这样我们就不必引发异常了。 - husayt
@husayt:代码已更新。现在当您传递“D”时,“A”将被返回。 - dance2die
26个回答

105

感谢大家的回答和反馈。我很惊讶能得到这么多的回复。查看了它们并使用其中一些想法,我想出了这个最适合我的解决方案:

public static class Extensions
{

    public static T Next<T>(this T src) where T : struct
    {
        if (!typeof(T).IsEnum) throw new ArgumentException(String.Format("Argument {0} is not an Enum", typeof(T).FullName));

        T[] Arr = (T[])Enum.GetValues(src.GetType());
        int j = Array.IndexOf<T>(Arr, src) + 1;
        return (Arr.Length==j) ? Arr[0] : Arr[j];            
    }
}

这种方法的优点是简单易用,而且适用范围广。它被实现为通用扩展方法,您可以在任何枚举类型上调用它,如下所示:
return eRat.B.Next();

注意,我正在使用通用扩展方法,因此在调用时不需要指定类型,只需使用 .Next() 即可。


1
大多数情况下对我有用。在Windows Phone Silverlight 8.0上,Type.IsEnum显然不存在于结构体中。删除类型检查程序,它就可以正常工作了。 - azarc3
8
为什么要使用 struct 而不是 where T : Enum - Vencovsky
你的回答似乎没有回答你的问题。它缺少了你问题中的“之前”的部分。 - Jogge

37

可能有点过头了,但:

eRat value = eRat.B;
eRat nextValue = Enum.GetValues(typeof(eRat)).Cast<eRat>()
        .SkipWhile(e => e != value).Skip(1).First();
或者,如果你想要第一个数值上更大的:
eRat nextValue = Enum.GetValues(typeof(eRat)).Cast<eRat>()
        .First(e => (int)e > (int)value);

或者对于下一个更大的数字(自己进行排序):

eRat nextValue = Enum.GetValues(typeof(eRat)).Cast<eRat>()
        .Where(e => (int)e > (int)value).OrderBy(e => e).First();

嘿,拿着LINQ这个工具,世界上的钉子都可以被敲打得服服帖帖 ;-p


20
"with LINQ as your hammer" -> 用LINQ作为你的工具,难道不是学习最快的方式吗?;) - dance2die
1
嘿,我知道我在挖坟了;-) 但是说到锤子,高性能情况下要小心使用像这样的结构(LINQ)。每个LINQ语句最终都会在内部分配一个列表。我认为这很酷,在一个非常大的项目中使用了这种语法,结果它很快成为了分配的#1热点。所以有时候,丑陋但干净胜过时尚和酷。 - Keith Bluestone
1
@KeithBluestone 不,不能说每个LINQ操作都会分配一个列表;大多数操作都是通过迭代器块完成的。需要缓冲的主要操作是OrderBy和GroupBy。 - Marc Gravell
1
@MarcGravell 理解了,谢谢。总的来说,我只是想让开发人员意识到,在友好、语义舒适的LINQ引擎下,可能会隐藏着大量的List<>分配问题。我确实是一个LINQ的粉丝,大多数情况下,这不是一个问题;但在编写一些高流量代码时,它却咬了我一口。 - Keith Bluestone
eRat value = eRat.D 时,这个不起作用 "InvalidOperationException Sequence contains no elements" http://rextester.com/live/YRGPO87367 - David d C e Freitas
显示剩余2条评论

17

你真的需要将这个问题泛化吗?你能否尝试直接这样做呢?

public void SomeMethod(MyEnum myEnum)
{
    MyEnum? nextMyEnum = myEnum.Next();

    if (nextMyEnum.HasValue)
    {
        ...
    }
}

public static MyEnum? Next(this MyEnum myEnum)
{
    switch (myEnum)
    {
        case MyEnum.A:
            return MyEnum.B;
        case MyEnum.B:
            return MyEnum.C;
        case MyEnum.C:
            return MyEnum.D;
        default:
            return null;
    }
}

3
+1 简单易懂。建立一个可以解决眼下需求的东西,然后继续前进。如果必要,以后再进行重构。 - Rex M
2
假设这就是后续的重构,这会有帮助吗? - Whatsit
47
我不喜欢这个,因为它很可能会成为一个需要维护的错误。当你添加一些新内容后,如果代码返回空值,那将是非常糟糕的一天。 - Eddie Parker
1
就像@EddieParker所说的那样。采用这种解决方案,您可能需要记住在更改枚举时存在该代码。从长远来看,更容易且更安全的方法是将其通用化,这样您可以安全地忘记它,并且在出现意外错误时无需查看此代码。 - Sebastiaan Hoedeman

12

在“D”之后该返回什么答案并没有得到解答,因此只能处理到“C”。

[更新1]:根据Marc Gravell的建议进行了更新。

[更新2]:根据husayt的要求进行了更新 - 返回下一个“D”的值为“A”。

public class Program
{
    public static void Main(string[] args)
    {
        Console.WriteLine("Next enum of A = {0}", eRatEnumHelper.GetNextEnumValueOf(eRat.A));
        Console.WriteLine("Next enum of B = {0}", eRatEnumHelper.GetNextEnumValueOf(eRat.B));
        Console.WriteLine("Next enum of C = {0}", eRatEnumHelper.GetNextEnumValueOf(eRat.C));
    }
}

public enum eRat { A = 0, B = 3, C = 5, D = 8 };

public class eRatEnumHelper
{
    public static eRat GetNextEnumValueOf(eRat value)
    {
        return (from eRat val in Enum.GetValues(typeof (eRat)) 
                where val > value 
                orderby val 
                select val).DefaultIfEmpty().First();
    }
}

结果

A的下一个枚举值为B
B的下一个枚举值为C
C的下一个枚举值为D
D的下一个枚举值为A


顺便说一下; .Take(1).ToArray()[0] - 可以简写为 .First() - Marc Gravell
天啊。如果又有人告诉我C#C++更好,我会向他展示这个示例,以显示(1)在C#中无法轻松获取下一个或上一个枚举值,以及(2)可以使用select来实现。哦我的天啊!(这不是关于你的问题,而是关于你可以在C#中做什么:有些人会说随心所欲做任何事情很好,有些人会说这是邪恶的。在我看来,这是邪恶的。) - Olivier Pons

12
你遇到的问题是因为你试图让枚举执行它不应该执行的操作。它们应该是类型安全的。将整数值分配给枚举是允许的,以便您可以组合它们,但如果您想要它们表示整数值,请使用类或结构体。以下是可能的替代方案:
public static class eRat
{
    public static readonly eRatValue A;
    public static readonly eRatValue B;
    public static readonly eRatValue C;
    public static readonly eRatValue D;

    static eRat()
    {
        D = new eRatValue(8, null);
        C = new eRatValue(5, D);
        B = new eRatValue(3, C);
        A = new eRatValue(0, B);
    }

    #region Nested type: ERatValue
    public class eRatValue
    {
        private readonly eRatValue next;
        private readonly int value;

        public eRatValue(int value, eRatValue next)
        {
            this.value = value;
            this.next = next;
        }

        public int Value
        {
            get { return value; }
        }

        public eRatValue Next
        {
            get { return next; }
        }

        public static implicit operator int(eRatValue eRatValue)
        {
            return eRatValue.Value;
        }
    }
    #endregion
}

这使您能够执行以下操作:
int something = eRat.A + eRat.B;

和这个

eRat.eRatValue current = eRat.A;
while (current != null)
{
    Console.WriteLine(current.Value);
    current = current.Next;
}

你应该只在需要类型安全时使用枚举。如果你依赖它们来表示一种类型,请转换为常量或类。
编辑
我建议你查看MSDN页面上的Enumeration Design。第一个最佳实践是:
使用枚举对表示值集的参数、属性和返回值进行强类型化。
我尽量避免争论教条,所以我不会这样做,但是你将面临的问题是:Microsoft不希望你做你正在尝试做的事情。他们明确要求你不要做你正在尝试做的事情。他们让你难以做你正在尝试做的事情。为了完成你正在尝试做的事情,你必须构建实用程序代码来强制其似乎可以工作。
你已经多次称呼你的解决方案为优雅,如果枚举被设计成不同的方式,那么它可能是优雅的,但是由于枚举是它们所拥有的,你的解决方案并不优雅。我认为室内乐是优雅的,但是如果音乐家没有合适的乐器,而必须用锯片和水壶演奏维瓦尔第,那么无论他们作为音乐家的能力如何,或者纸上的音乐有多好,它都不再优雅。

1
看到这个,我希望我能再点赞一次。看起来很明显,但我想我从未真正做过这样的事情。 - Greg D
请查看我的回答,我在其中解释了为什么我需要一个枚举。如果您能在这种情况下看到一个可行的解决方案,我将非常乐意接受它。 - husayt

7
感谢大家提供的灵感和解决方案。
以下是我的成果,作为扩展功能。
using System; 
using System.Linq;

public static class Enums
{
    public static T Next<T>(this T v) where T : struct
    {
        return Enum.GetValues(v.GetType()).Cast<T>().Concat(new[] { default(T) }).SkipWhile(e => !v.Equals(e)).Skip(1).First();
    }

    public static T Previous<T>(this T v) where T : struct
    {
        return Enum.GetValues(v.GetType()).Cast<T>().Concat(new[] { default(T) }).Reverse().SkipWhile(e => !v.Equals(e)).Skip(1).First();
    }
}

使用:

using System; 
using System.Linq;

public enum Test { F1, F2, F3 }

public class Program
{
    public static void Main()
    {
        Test t = Test.F3;   
        
        Console.WriteLine(t);
        Console.WriteLine(t.Next());
        Console.WriteLine(t.Previous());
        
        Console.WriteLine("\n");
        
        t = Test.F1;    
        Console.WriteLine(t);
        Console.WriteLine(t.Next());
        Console.WriteLine(t.Previous());
    }
}

结果:

F3
F1
F2

F1
F2
F3

2
不要忘记写上 using System;using System.Linq; - user2513149
2
@user2513149,感谢您提供的信息,我们已经在这里修复了。不知道为什么那个东西会丢失。 - NOFILENAMED
1
点赞,因为代码中包含了导入! :-) 说真的,有些人开始觉得不显示导入很“时髦”了。 - Xan-Kun Clark-Davis

4

您是否因为某些您无法控制的因素而被限制使用枚举类型?

如果不是这样的话,我建议您使用另一种替代方案——Dictionary<string, int> rat;

如果您创建了一个 Dictionary 并填充了数据,遍历它会更加简单。此外,这是一种更清晰的映射意图方式——你正在将数字映射到字符串上,并试图利用该映射。

如果必须使用枚举类型,我建议您考虑其他方案:

var rats = new List<eRat>() {eRat.A, eRat.B, eRat.C, eRat.D};

只要您按顺序添加值并保持同步,就可以大大简化检索下一个eRat的操作。

4
根据您的描述,您并不真正需要一个枚举。您正在超出枚举的能力范围。为什么不创建一个自定义类,将您需要的值公开为属性,同时将它们保留在有序字典中。然后获取下一个/上一个将是微不足道的。
如果您想基于上下文以不同方式对集合进行枚举,请将其明确作为设计的一部分。将项封装在一个类中,并且每个方法都返回IEnumerable,其中T是您想要的类型。
例如:
IEnumerable<Foo> GetFoosByBar()
IEnumerable<Foo> GetFoosByBaz()

etc...


1
好的建议 - 不要过度使用枚举。 - Gordon Mackie JoanMiro
我不明白。你为什么要在枚举上使用foreach?你能提供更多的上下文吗? - Krzysztof Kozmic
不是在枚举本身上,而是要能够以不同的方式枚举数组成员。我使用枚举作为固定索引器。 - husayt
请检查我的答案,如需更详细的解释,请告诉我。谢谢。 - husayt

4

对于简单的解决方案,您可以从枚举中提取数组。

eRat[] list = (eRat[])Enum.GetValues(typeof(eRat));

然后您可以枚举。
foreach (eRat item in list)
    //Do something

或查找下一个项目

int index = Array.IndexOf<eRat>(list, eRat.B);
eRat nextItem = list[index + 1];

将数组存储起来比每次从枚举中提取下一个值更好。

但如果您想要更美观的解决方案,请创建类。

public class EnumEnumerator<T> : IEnumerator<T>, IEnumerable<T> {
    int _index;
    T[] _list;

    public EnumEnumerator() {
        if (!typeof(T).IsEnum)
            throw new NotSupportedException();
        _list = (T[])Enum.GetValues(typeof(T));
    }
    public T Current {
        get { return _list[_index]; }
    }
    public bool MoveNext() {
        if (_index + 1 >= _list.Length)
            return false;
        _index++;
        return true;
    }
    public bool MovePrevious() {
        if (_index <= 0)
            return false;
        _index--;
        return true;
    }
    public bool Seek(T item) {
        int i = Array.IndexOf<T>(_list, item);
        if (i >= 0) {
            _index = i;
            return true;
        } else
            return false;
    }
    public void Reset() {
        _index = 0;
    }
    public IEnumerator<T> GetEnumerator() {
        return ((IEnumerable<T>)_list).GetEnumerator();
    }
    void IDisposable.Dispose() { }
    object System.Collections.IEnumerator.Current {
        get { return Current; }
    }
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {
        return _list.GetEnumerator();
    }
}

实例化

var eRatEnum = new EnumEnumerator<eRat>();

迭代

foreach (eRat item in eRatEnum)
    //Do something

MoveNext

eRatEnum.Seek(eRat.B);
eRatEnum.MoveNext();
eRat nextItem = eRatEnum.Current;

3

您可以将其简化并概括:

static Enum GetNextValue(Enum e){
    Array all = Enum.GetValues(e.GetType());
    int i = Array.IndexOf(all, e);
    if(i < 0)
        throw new InvalidEnumArgumentException();
    if(i == all.Length - 1)
        throw new ArgumentException("No more values", "e");
    return (Enum)all.GetValue(i + 1);
}

编辑: 请注意,如果您的枚举包含重复值(同义词条目),则使用此方法(或此处列出的任何其他技术)将会失败,给定其中一个值。例如:

enum BRUSHSTYLE{
    SOLID         = 0,
    HOLLOW        = 1,
    NULL          = 1,
    HATCHED       = 2,
    PATTERN       = 3,
    DIBPATTERN    = 5,
    DIBPATTERNPT  = 6,
    PATTERN8X8    = 7,
    DIBPATTERN8X8 = 8
}

如果给定的是BRUSHSTYLE.NULL或者BRUSHSTYLE.HOLLOW,那么返回值将会是BRUSHSTYLE.HOLLOW

<leppie>

Update: a generics version:

static T GetNextValue<T>(T e)
{
  T[] all = (T[]) Enum.GetValues(typeof(T));
  int i = Array.IndexOf(all, e);
  if (i < 0)
    throw new InvalidEnumArgumentException();
  if (i == all.Length - 1)
    throw new ArgumentException("No more values", "e");
  return all[i + 1];
}

</leppie>

@leppie:

你的通用版本允许意外传递非枚举值,这只能在运行时捕获。我最初是将其编写为通用版本,但当编译器拒绝 where T : Enum 时,我将其删除,并意识到我并没有从通用性中获得太多好处。唯一的真正缺点是你必须将结果强制转换回你特定的枚举类型。


来吧!不要用泛型,别偷懒 :) - leppie
@leppie:请查看帖子中的评论。 - P Daddy
您可以在方法内部添加一个检查。或者你也可以不添加,因为它无论如何都会出问题 :) - leppie
我的观点是非泛型版本将在编译时捕获您的错误。我总是更喜欢在编译时捕获错误,而不是在运行时捕获它们。 - P Daddy
为了使通用版本更加安全,您可以添加“where T:struct”,然后再添加以下代码以进一步强制执行: " if (!typeof(T).IsEnum) throw new ArgumentException(String.Format("Argumnent {0} is Not an Enum", typeof(T).FullName)); " - husayt
@husayt:这真的没有好多少。这仍然会留下许多常见情况,其中运行时检查取代了编译时检查。我和其他人一样喜欢泛型(也许更喜欢),但我真的不明白为什么人们如此执着于使这种特定方法成为泛型。 - P Daddy

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