如何优雅地编写一个函数,以从IEnumerable中获取唯一的属性(如果存在)?

3

我想编写一个函数,用于遍历IEnumerable。对于IEnumerable中的每个项目,它会获取一个枚举属性。如果IEnumerable中的所有项目都具有该属性的相同值,则返回该值。否则,返回null。我可以做到这一点,但不够优雅。是否有Linq表达式可用?请参见下面的UniqueOption函数。

namespace Play
{
public enum Option
{
    Tom,
    Dick,
    Harry
}

public class OptionHolder
{
    public Option Option { get; set; }

    public override string ToString()
    {
        return Option.ToString();
    }
}

public class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    private static void Main()
    {
        Program p1 = new Program(Option.Tom, Option.Dick, Option.Harry);
        Console.WriteLine("1: "+p1.UniqueOption());     //should be null
        Program p2 = new Program(Option.Dick, Option.Dick, Option.Dick);
        Console.WriteLine("2: " + p2.UniqueOption());   //should be Dick
        Program p3 = new Program(Option.Harry);         
        Console.WriteLine("3: " + p3.UniqueOption());   //should be Harry
    }

    public Program(params Option[] options)
    {
        optionList = new List<OptionHolder>();
        foreach (Option option in options)
        {
            OptionHolder holder = new OptionHolder();
            holder.Option = option;
            optionList.Add(holder);
        }
    }

    /**
     * If all the OptionHolders in the Holders property have the same Option, return this.
     * Otherwise (there are no OptionHolders, or there is more than one but they hold different Options), return null.
     */
    public Option? UniqueOption()
    {
        Option? option = null;

        foreach(OptionHolder holder in optionList) {
            Option o = holder.Option;
            if (option == null)
            {
                option = o;
            }
            else if (option != o)
            {
                return null;
            }
        }
        return option;
    }

    private List<OptionHolder> optionList;

    public IEnumerable<OptionHolder> Holders
    {
        get { return optionList; }
    }

    public override string ToString()
    {
        return String.Join(",", optionList);
    }
}

}

6个回答

2
如果我理解正确,您可以使用Linq的Distinct方法。
public Option? UniqueOption()
{
    var distinct = optionList.Select(x=> x.Option).Distinct();
    if(distinct.Count() == 1)
    {
        return distinct.First();
    }
    return null;
}

public Option? UniqueOptionOptimized()
{
    HashSet<Option> set = new HashSet<Option>();
    foreach (var item in optionList)
    {
        if (set.Add(item.Option) && set.Count > 1)
        {
            return null;
        }
    }

    if (set.Count == 1)
        return set.First();
    else
        return null;
}

public Option? UniqueOptionOptimized2()
{
    using(var distinctEnumerator = optionList.Select(x => x.Option).Distinct().GetEnumerator())
    {
        if(distinctEnumerator.MoveNext())
        {
            var firstOption = distinctEnumerator.Current;
            if(!distinctEnumerator.MoveNext())
                return firstOption;
        }
    }
    return null;
}

2
@PaulRichards:我相信你想要桶里的威士忌和醉了的妻子同时得到。Linq什么都有,但并不高效!你要么追求优雅多变的Linq方式,要么选择优化但困难的方式。我的解决方案和Sriram的第一个解决方案都是Linq,其他的不是。 - Mario Vernari
我知道Linq并不以其效率而闻名。然而,Any方法并不一定要遍历每个项。我已经有了我的解决方案,但我会再考虑一下这个问题。 - Paul Richards
@MichaelLiu 很好的发现,已更新 :) - Sriram Sakthivel
1
LINQ在一般情况下并不特别低效;事实上,大多数人倾向于认为它比实际低效得多。当然,如果没有正确使用它(例如,使用可以短路的方法来迭代整个序列),会导致性能变差。 - Servy
@Servy:你说得对!现在我明白了诀窍所在:非常好。 - Mario Vernari
显示剩余5条评论

1
使用Distinct()Take(2)一起,当找到两个不同的选项时停止枚举列表,然后检查是否恰好找到一个不同的选项(而不是零个或两个):
public Option? UniqueOption()
{
    Option[] options = optionList.Select(holder => holder.Option).Distinct().Take(2).ToArray();
    return options.Length == 1 ? options[0] : (Option?)null;
}

更新:为了检查实际枚举的值有多少个,您可以使用以下辅助方法:

public static IEnumerable<T> Trace<T>(IEnumerable<T> values)
{
    foreach (T value in values)
    {
        Console.WriteLine("Yielding {0}...", value);
        yield return value;
    }
}

像这样调用它:
Option[] options = Trace(optionList).Select(holder => holder.Option).Distinct().Take(2).ToArray();

这表明 p1 只枚举了 TomDick,而不是 Harry

这看起来不错,Michael。你确定它只枚举了足够的值来获得两个不同的值吗?我可能会测试一下看看。 - Paul Richards
谢谢Michael,你说得对 - 那是一种优雅而高效的解决方案。 - Paul Richards

0
下面的函数应该适合你的需要。不过,我使用了一个更简单的应用程序进行了尝试,但你应该能够根据自己的意愿进行调整。
    public Option? UniqueOption(params Option[] args)
    {
        if (args == null)
            return null;  //or maybe throws?

        var d = args.Distinct().ToArray();
        return d.Length != 1
            ? d[0]
            : new Nullable<Option>();
    }

2
如果 d.Length == 0,则 d[0] 引发异常。 - Grundy
这与Sriram Sakthivel的解决方案存在相同的性能问题。 - Paul Richards

0
[STAThread]
private static void Main()
{

    Option tested = Option.Paul;
    Program p4 = new Program(Option.Paul, Option.Paul, Option.Paul);
    Console.WriteLine("4: " + p4.UniqueOption(tested));

}
public Option? UniqueOption(Option tested)
{
    if (optionList.All(o => o.Option == tested))
        return tested;

    return null;
}

1
我的UniqueOption函数没有参数。你的解决方案不符合我的要求,因为我们不知道唯一选项的值是什么。 - Paul Richards

0

另一种变体:

public Option? UniqueOption()
{
    if(!optionList.Any()) return null;
    var first = optionList.First();
    return optionList.All(a => first.Option == a.Option) ? (Option?)first.Option : null;
}

这是一个简单直接的解决方案。然而,如果optionList是一个延迟查询而不是List<OptionHolder>,那么查询将被多次评估。 - Michael Liu
@MichaelLiu 是的,但在示例中它是列表 :-) - Grundy

0

最终我使用了与我的原始解决方案并没有太大区别的东西。不过等我有更多时间时,我会再次仔细研究一下。

public Option? UniqueOption()
{            
        IEnumerator<OptionHolder> enumerator = Holders.GetEnumerator();
        bool hasMoreElements;
        Option? result=null;
        do
        {
           hasMoreElements = enumerator.MoveNext();
           if (hasMoreElements)
           {
              Option? option = enumerator.Current.Option;
              result = (result != null && result != option) ? null : option; 
           }
        } 
        while (hasMoreElements && result != null);
        return result; 
} 

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