如何在 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 == 0
和 b == 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");
}
鉴于:
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 == 0
和 b == 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");
}