【问题标题】:How to null check c# 7 tuple in LINQ query?如何在 LINQ 查询中对 c# 7 元组进行空检查?
【发布时间】:2017-11-02 14:37:39
【问题描述】:

给定:

class Program
{
    private static readonly List<(int a, int b, int c)> Map = new List<(int a, int b, int c)>()
    {
        (1, 1, 2),
        (1, 2, 3),
        (2, 2, 4)
    };

    static void Main(string[] args)
    {
        var result = Map.FirstOrDefault(w => w.a == 4 && w.b == 4);

        if (result == null)
            Console.WriteLine("Not found");
        else
            Console.WriteLine("Found");
    }
}

在上面的示例中,在if (result == null) 行遇到了编译器错误。

CS0019 运算符“==”不能应用于“(int a, int b, int c)”和“”类型的操作数

在继续我的“找到”逻辑之前,我将如何检查是否找到了元组?

在使用新的 c# 7 元组之前,我会这样:

class Program
{
    private static readonly List<Tuple<int, int, int>> Map = new List<Tuple<int, int, int>>()
    {
        new Tuple<int, int, int> (1, 1, 2),
        new Tuple<int, int, int> (1, 2, 3),
        new Tuple<int, int, int> (2, 2, 4)
    };

    static void Main(string[] args)
    {
        var result = Map.FirstOrDefault(w => w.Item1 == 4 && w.Item2 == 4);

        if (result == null)
            Console.WriteLine("Not found");
        else
            Console.WriteLine("Found");
    }
}

效果很好。我喜欢新语法更容易解释的意图,但不确定如何在对找到的内容采取行动(或不采取行动)之前对其进行空检查。

【问题讨论】:

  • 值元组是值类型。它们不能为空
  • 在这种情况下,默认值为 3 个零的元组,而不是 null。所以你可以检查一下。
  • @juharr 但如果列表包含 (0,0,0) 项,这将中断,因此不太可靠。
  • @Evk (0,0,0) 在这种情况下与谓词不匹配,但在其他情况下可能是个问题。
  • @PanagiotisKanavos 不正确。如果集合包含 (0,0,0) 并且谓词匹配它怎么办?然后您必须使用Any 进行预先检查,或者使用Take(1).ToArray() 进行xanatos 的技巧来区分未找到和找到,但恰好与默认值匹配。

标签: c# linq c#-7.0


【解决方案1】:

值元组是值类型。它们不能为空,这就是编译器抱怨的原因。旧的 Tuple 类型是引用类型

在这种情况下,FirstOrDefault() 的结果将是 ValueTuple&lt;int,int,int&gt; 的默认实例 - 所有字段都将设置为其默认值 0。

如果要检查默认值,可以将结果与默认值ValueTuple&lt;int,int,int&gt;进行比较,例如:

var result=(new List<(int a, int b, int c)>()
            {
                (1, 1, 2),
                (1, 2, 3),
                (2, 2, 4)
            }
        ).FirstOrDefault(w => w.a == 4 && w.b == 4);

if (result.Equals(default(ValueTuple<int,int,int>)))
{
    Console.WriteLine("Missing!"); 
}

警告词

该方法称为FirstOrDefault,而不是TryFirst。它并不是要检查一个值是否存在,尽管我们都(ab)以这种方式使用它。

在 C# 中创建这样的扩展方法并不难。经典选项是使用 out 参数:

public static bool TryFirst<T>(this IEnumerable<T> seq,Func<T,bool> filter, out T result) 
{
    result=default(T);
    foreach(var item in seq)
    {
        if (filter(item)) {
            result=item;
            return true;
         }
    }
    return false;
}

在 C# 7 中调用它可以简化为:

if (myList.TryFirst(w => w.a == 4 && w.b == 1,out var result))
{
    Console.WriteLine(result);
}

F# 开发人员可以吹嘘他们有一个Seq.tryPick,如果找不到匹配项,它将返回None

C# 没有 Option 类型或 Maybe 类型(目前),但也许(双关语)我们可以构建自己的:

class Option<T> 
{
    public T Value {get;private set;}

    public bool HasValue {get;private set;}

    public Option(T value) { Value=value; HasValue=true;}    

    public static readonly Option<T> Empty=new Option<T>();

    private Option(){}

    public void Deconstruct(out bool hasValue,out T value)
    {
        hasValue=HasValue;
        value=Value;
    }
}

public static Option<T> TryPick<T>(this IEnumerable<T> seq,Func<T,bool> filter) 
{
    foreach(var item in seq)
    {
        if (filter(item)) {
            return new Option<T>(item);
         }
    }
    return Option<T>.Empty;
}

这允许编写以下 Go 风格的调用:

var (found,value) =myList.TryPick(w => w.a == 4 && w.b == 1);

除了比较传统的:

var result=myList.TryPick(w => w.a == 4 && w.b == 1);
if (result.HasValue) {...}

【讨论】:

  • 但是发布的 LINQ 查询显然不会在列表中找到 ab 为 4 的元组,那么我将如何验证呢?
  • 你会得到一个默认值——一个所有字段都设置为默认值的元组,即 0。
  • 这可能是Map 中的一个有效案例,我实际上并没有使用几个ints,它只是作为示例。
  • 在 C# 7.1 中,您现在可以只写:if (result.Equals(default)),它会自动推断 default 的类型。要使用 C# 7.1,您需要最新版本的 Visual Studio 2017,并在项目构建设置中将 C# 版本设置为 7.1 (Advanced...)。
  • Option 非常适合成为struct,类似于Nullable。否则你需要处理一个令人困惑的可选的可空性。
【解决方案2】:

正如 Panagiotis 所写,你不能直接这样做......你可以“欺骗”一点:

var result = Map.Where(w => w.a == 4 && w.b == 4).Take(1).ToArray();

if (result.Length == 0)
    Console.WriteLine("Not found");
else
    Console.WriteLine("Found");

Where 最多取一个元素,并将结果放入长度为 0-1 的数组中。

或者你可以重复比较:

var result = Map.FirstOrDefault(w => w.a == 4 && w.b == 4);

if (result.a == 4 && result.b == 4)
    Console.WriteLine("Not found");

如果您正在寻找,第二个选项将不起作用

var result = Map.FirstOrDefault(w => w.a == 0 && w.b == 0);

在这种情况下,FirstOrDefault() 返回的“默认”值 a == 0b == 0

或者您可以简单地创建一个具有out bool success 的“特殊”FirstOrDefault()(如各种TryParse):

static class EnumerableEx
{
    public static T FirstOrDefault<T>(this IEnumerable<T> source, Func<T, bool> predicate, out bool success)
    {
        if (source == null)
        {
            throw new ArgumentNullException(nameof(source));
        }

        if (predicate == null)
        {
            throw new ArgumentNullException(nameof(predicate));
        }

        foreach (T ele in source)
        {
            if (predicate(ele))
            {
                success = true;
                return ele;
            }
        }

        success = false;
        return default(T);
    }
}

像这样使用它:

bool success;
var result = Map.FirstOrDefault(w => w.a == 4 && w.b == 4, out success);

其他可能的扩展方法,ToNullable&lt;&gt;()

static class EnumerableEx
{
    public static IEnumerable<T?> ToNullable<T>(this IEnumerable<T> source) where T : struct
    {
        return source.Cast<T?>();
    }
}

像这样使用它:

var result = Map.Where(w => w.a == 4 && w.b == 4).ToNullable().FirstOrDefault();

if (result == null)

请注意,resultT?,因此您需要执行 result.Value 才能使用其值。

【讨论】:

    【解决方案3】:

    您的支票可能如下:

    if (!Map.Any(w => w.a == 4 && w.b == 4))
    {
        Console.WriteLine("Not found");
    }
    else
    {
        var result = Map.First(w => w.a == 4 && w.b == 4);
        Console.WriteLine("Found");
    }
    

    【讨论】:

    • 我认为您不需要在第一个 if 中使用 result ==
    • 你的语法有点不对劲,第一个 if 应该是 if (Map.Any(...)),但这仍然不能以所需的方式工作,它总是“找到”
    • 如果您更正此答案的语法,这似乎是最简单的解决方案if (!Map.Any(w =&gt; w.a == 4 &amp;&amp; w.b == 4))
    • @Kritner 如果更改为 if(!Map.Any(w =&gt; w.a == 4 &amp;&amp; w.b == 4)),它会起作用,但对于所有“找到”的情况,它会重复 Map 两次。
    • 啊,你是对的@Scrobi。它会迭代两次,这可能是大型数据集的问题。如果您的数据集较小或性能不是一个大问题,那么在我看来,提高可读性是值得的。
    【解决方案4】:

    只是再添加一种处理值类型和FirstOrDefault 的替代方法:使用Where 并将结果转换为可空类型:

    var result = Map.Where(w => w.a == 4 && w.b == 4)
       .Cast<(int a, int b, int c)?>().FirstOrDefault();
    
    if (result == null)
       Console.WriteLine("Not found");
    else
       Console.WriteLine("Found");
    

    您甚至可以对其进行扩展:

    public static class Extensions {
        public static T? StructFirstOrDefault<T>(this IEnumerable<T> items, Func<T, bool> predicate) where T : struct {
            return items.Where(predicate).Cast<T?>().FirstOrDefault();
        }
    }
    

    然后您的原始代码将编译(假设您将FirstOrDefault 替换为StructFirstOrDefault)。

    【讨论】:

    • 你不能把它命名为FirstOrDefault():重载决议不是基于通用限制完成的。
    • @xanatos 你是对的 - 这不是最好的名字,改变了它(尽管它仍然可以编译并与 OP 示例代码一起正常工作)。
    • 或者干脆叫它FirstOrNull,因为它的目的是在没有找到任何东西时返回一个空值。 +1
    【解决方案5】:

    ValueTuple 是用于 C#7 元组的基础类型。它们不能为空,因为它们是值类型。您可以测试它们的默认值,但这实际上可能是一个有效值。

    另外,等式运算符没有在 ValueTuple 上定义,所以你必须使用 Equals(...)。

    static void Main(string[] args)
    {
        var result = Map.FirstOrDefault(w => w.Item1 == 4 && w.Item2 == 4);
    
        if (result.Equals(default(ValueTuple<int, int, int>)))
            Console.WriteLine("Not found");
        else
            Console.WriteLine("Found");
    }
    

    【讨论】:

      【解决方案6】:

      如果您确定您的数据集不包含(0, 0, 0),那么正如其他人所说,您可以检查默认值:

      if (result.Equals(default(ValueTuple<int,int,int>))) ...
      

      如果该值可能出现,那么您可以使用 First 并在不匹配时捕获异常:

      class Program
      {
          private static readonly List<(int a, int b, int c)> Map = 
              new List<(int a, int b, int c)>()
          {
              (1, 1, 2),
              (1, 2, 3),
              (2, 2, 4),
              (0, 0, 0)
          };
      
          static void Main(string[] args)
          {
              try
              {
                  Map.First(w => w.a == 0 && w.b == 0);
                  Console.WriteLine("Found");
              }
              catch (InvalidOperationException)
              {
                  Console.WriteLine("Not found");
              }
          }
      }
      

      或者,您可以使用库 such as my own Succinc<T> library 提供 TryFirst 方法,如果不匹配则返回“可能”类型的 none,或者如果匹配则返回项目:

      class Program
      {
          private static readonly List<(int a, int b, int c)> Map = 
              new List<(int a, int b, int c)>()
          {
              (1, 1, 2),
              (1, 2, 3),
              (2, 2, 4),
              (0, 0, 0)
          };
      
          static void Main(string[] args)
          {
              var result = Map.TryFirst(w => w.a == 0 && w.b == 0);
              Console.WriteLine(result.HasValue ? "Found" : "Not found");
          }
      }
      

      【讨论】:

      • 您可以将Deconstruct 添加到Option,也许是为了获得Go 风格的结果,例如var (found,value) = Map.TryFirst(...)
      • @PanagiotisKanavos,嘿,这真是个好主意。我会将其添加到下一个版本的功能列表中。谢谢。
      【解决方案7】:

      上面的大多数答案都暗示您的结果元素不能是默认的(T),其中 T 是您的类/元组。

      一种简单的解决方法是使用如下方法:

      var result = Map
         .Select(t => (t, IsResult:true))
         .FirstOrDefault(w => w.t.Item1 == 4 && w.t.Item2 == 4);
      
      Console.WriteLine(result.IsResult ? "Found" : "Not found");
      

      此示例使用 C# 7.1 隐含的元组名称(以及 C# 7 的 ValueTuple 包),但如果需要,您可以为元组元素显式指定名称,或者使用简单的 Tuple&lt;T1,T2&gt; 代替。

      【讨论】:

        【解决方案8】:

        你需要:

        if (result.Equals(default)) Console.WriteLine(...
        

        (c# > 7.1)

        【讨论】:

          【解决方案9】:

          我是如何使用 c# 7.3 做到的

          T findme;
          var tuple = list.Select((x, i) => (Item: x, Index: i)).FirstOrDefault(x => x.Item.GetHashCode() == findme.GetHashCode());
          
          if (tuple.Equals(default))
              return;
          
          ...
          var index = tuple.Index;
          

          【讨论】:

            【解决方案10】:

            在 C# 7.3 中,它非常干净:

            var result = Map.FirstOrDefault(w => w.a == 4 && w.b == 4);
            if (result == default) {
                Console.WriteLine("Not found");
            } else {
                Console.WriteLine("Found");
            }
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-09-15
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多