Как null проверить кортеж c# 7 в запросе LINQ?
дано:
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");
}
}
, который работал хорошо. Мне нравится больше легко интерпретируемое намерение нового синтаксиса, но я не уверен, как его проверить до того, как действовать на то, что было найдено (или нет).
7 ответов
кортежи значений-это типы значений. Они не могут быть null, поэтому компилятор жалуется. Старый тип Кортежа был ссылочным типом
результат 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# не так сложно. Классический вариант-использовать параметр 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# не имеет типов опций или типа 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-style:
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) {...}
просто добавить еще одну альтернативу для работы с типами значений и FirstOrDefault
используйте Where
и приведите результат к типу nullable:
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
).
как написано 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);
в этом случае значение "default", возвращаемое FirstOrDefault()
и a == 0
и b == 0
.
или вы можете просто создать "специальный"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
использовать его значение.
если вы уверены, что ваш набор данных не включают (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");
}
}
}
кроме того, вы можете использовать библиотеку,например, моя собственная библиотека SuccincTryFirst
метод, который возвращает тип "возможно"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");
}
}
ваш чек может быть следующим:
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");
}
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");
}
большинство ответов выше подразумевают, что ваш результирующий элемент не может быть default(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 (и пакет ValueTuple для C# 7), но при необходимости можно явно указать имя элементов кортежа или использовать простой Tuple<T1,T2>
вместо.