Как проверить нулевой набор кортежей С# 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)' и ''
Как я могу проверить, что кортеж найден до начала моей "найденной" логики?
До использования новых кортежей С# 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");
}
}
Это сработало нормально. Мне нравится более легко интерпретируемое намерение нового синтаксиса, но я не уверен, как его проверить, прежде чем действовать на то, что было найдено (или нет).
Ответы
Ответ 1
Корзины значений - это типы значений. Они не могут быть пустыми, поэтому компилятор жалуется. Старый тип Tuple был ссылочным типом
Результат 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) используем его таким образом.
Создание такого метода расширения в С# не так уж сложно. Классическим вариантом является использование параметра 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;
}
Вызов этого можно упростить в С# 7 как:
if (myList.TryFirst(w => w.a == 4 && w.b == 1,out var result))
{
Console.WriteLine(result);
}
Разработчики F # могут похвастаться тем, что у них есть Seq.tryPick, который вернет None
, если совпадение не найдено.
У С# нет типов опций или типа 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) {...}
Ответ 2
Просто добавьте еще одну альтернативу для использования типов значений и FirstOrDefault
: используйте Where
и примените результат к типу NULL:
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
).
Ответ 3
Как написано 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 == 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
, чтобы использовать его значение.
Ответ 4
Если вы уверены, что ваш набор данных не будет включать (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");
}
}
}
В качестве альтернативы вы можете использовать библиотеку 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");
}
}
Ответ 5
Ваш чек может быть следующим:
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");
}
Ответ 6
ValueTuple - это базовый тип, используемый для кортежей С# 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");
}
Ответ 7
Вам нужно:
if (result.Equals(default)) Console.WriteLine(...
(c #> 7.1)
Ответ 8
Большинство ответов выше подразумевают, что ваш результирующий элемент не может быть значением по умолчанию (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");
В этом примере используются подразумеваемые имена кортежей С# 7.1 (и пакет ValueTuple для С# 7), но при необходимости вы можете явно дать имя своим элементам кортежа или использовать вместо него простой Tuple<T1,T2>
.
Ответ 9
как я это сделал с С# 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
В С# 7.3 это очень чисто:
var result = Map.FirstOrDefault(w => w.a == 4 && w.b == 4);
if (result == default) {
Console.WriteLine("Not found");
} else {
Console.WriteLine("Found");
}