如何在 LINQ 查询中空检查 c# 7 元组?

How to null check c# 7 tuple in LINQ query?

鉴于:

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 Operator '==' cannot be applied to operands of type '(int a, int b, int c)' and '<null>'

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

在使用新的 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");
    }
}

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

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

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

如果要检查默认值,可以将结果与ValueTuple<int,int,int>的默认值进行比较,例如:

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# 中创建这样的扩展方法并不难。经典选项是使用输出参数:

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,如果找不到匹配项,它将 return 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) {...}

正如 Panagiotis 所写,你不能直接这样做......你可以 "cheat" 一点:

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() 返回的 "default" 值具有 a == 0b == 0

或者您可以简单地创建一个 "special" FirstOrDefault(),它有一个 out bool success(就像各种 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<>()

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)

请注意 result 是一个 T?,因此您需要执行 result.Value 才能使用它的值。

您的支票可能如下:

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");
}

只是为了再添加一种处理值类型的替代方法 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)。

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

此外,相等运算符未在 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");
}

如果您确定您的数据集不会包含 (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 方法,如果不匹配,returns "maybe" 类型的 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");
    }
}

上面的大多数答案都暗示你的结果元素不能是 default(T),其中 T 是你的 class/tuple.

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

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<T1,T2>

你需要:

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

(c# > 7.1)

我是如何用 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;

在 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");
}