C
C#2mo ago
Olipro

Selecting and filtering from a list with full nullable safety & warnings as errors

I have the type Btn in my sample below which simulates the portion of WinForms that I'm interested in. I have a list of Btn objects and my goal is to mutate it into a subset of Btn objects that have their Tag matching the Wanted type without being opaque to the static analyser that detects potential null references (and avoiding the null-forgiving operator) I ran the code below over on https://sharplab.io and it does not complain that OfType could return any nullables. Nonetheless, the code fails because the middle element (which will be a tuple of nulls) still gets returned. Is this an issue with the REPL or is the static analyser really not seeing that the tuple types could end up being null? Perhaps there is a cleaner way to achieve what I want. Do note that I cannot alter the type held by Btn.Tag, it's an object in WinForms and I have no control over the code that decided to expose the types via that field.
#nullable enable

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

class Btn {
public object Tag { get; private set; }
public string Str { get; private set; }
public Btn(object tag, string str) {
Tag = tag;
Str = str;
}
}

class Wanted {}
class Unwanted {}

class main {
public static void Main(string[] args) {
List<Btn> lst = [new Btn(new Wanted(), "A"),
new Btn(new Unwanted(), "B"),
new Btn(new Wanted(), "C")];

foreach (var tup in lst.Select(btn => btn.Tag is Wanted w ? (btn, w) : (null, null)).OfType<(Btn, Wanted)>())
Console.WriteLine(tup.Item1.Str);
}
}
#nullable enable

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

class Btn {
public object Tag { get; private set; }
public string Str { get; private set; }
public Btn(object tag, string str) {
Tag = tag;
Str = str;
}
}

class Wanted {}
class Unwanted {}

class main {
public static void Main(string[] args) {
List<Btn> lst = [new Btn(new Wanted(), "A"),
new Btn(new Unwanted(), "B"),
new Btn(new Wanted(), "C")];

foreach (var tup in lst.Select(btn => btn.Tag is Wanted w ? (btn, w) : (null, null)).OfType<(Btn, Wanted)>())
Console.WriteLine(tup.Item1.Str);
}
}
32 Replies
Angius
Angius2mo ago
I'd recommend filtering it out with .Where() first
Olipro
OliproOP2mo ago
As an addendum, I am aware that I can chain .Where(tup => tup.Item1 is not null) but that seems silly when OfType is supposed to remove nullables
Angius
Angius2mo ago
Probably has to do with how reference types are nullable by default. So all elements that your Select() returns are of type (Btn?, Wanted?) null is not really a type You could not do .OfType<null>() for example
Olipro
OliproOP2mo ago
I have set the pragma #nullable enable it would emit a warning It has occurred to me that I can make it work correctly by explicitly stating the tuple as nullable - like so:
lst.Select<Btn, (Btn, Wanted)?>(btn => btn.Tag is Wanted w ? (btn, w) : null).OfType<(Btn, Wanted)>()
lst.Select<Btn, (Btn, Wanted)?>(btn => btn.Tag is Wanted w ? (btn, w) : null).OfType<(Btn, Wanted)>()
This functions correctly, but it does strike me as a bit verbose
Angius
Angius2mo ago
A .Where() would be simpler
Olipro
OliproOP2mo ago
.Where() is not visible to the analyser, it won't know that the tuple's types can't be null or more specifically, .Where() will filter out the nullables, but it won't transform the type from T?, T2? to T, T2
Angius
Angius2mo ago
True
Angius
Angius2mo ago
Unfortulately, nullability for reference types in C# is a bit of a "landlord special"
No description
Angius
Angius2mo ago
Just about kinda works, but... not that well
Olipro
OliproOP2mo ago
well, I have a working solution now, as posted above, but it'd be cosmetically pleasant if I could do it more tersely than explicitly writing out the tuple as a nullable type to make it cooperate
ero
ero2mo ago
this seems like an issue with tuples specifically
Olipro
OliproOP2mo ago
This makes it cleaner:
lst.Select(btn => btn.Tag is Wanted w ? (btn, w) : default((Btn, Wanted)?)).OfType<(Btn, Wanted)>())
lst.Select(btn => btn.Tag is Wanted w ? (btn, w) : default((Btn, Wanted)?)).OfType<(Btn, Wanted)>())
the right hand side of the ternary is no longer ambiguous so I don't have to specify the generics if anyone thinks they can make it shorter, I'm all ears.
ero
ero2mo ago
yeah the problem is that foo is (C, D) does a tuple pattern check. it checks the items individually. but OfType will do is ValueTuple<C, D>. and that can't ever be true, because generic parameters in structs don't have variance
Olipro
OliproOP2mo ago
if that is the case, I'd expect it to still fail nullable analysis
ero
ero2mo ago
how do you mean?
Olipro
OliproOP2mo ago
if the return type of Select is an IEnumerable<(T?, T2?)> then it should fail when OfType is supposed to be returning an IEnumerable<(T, T2)> in my original code sample, the ternary was returning either (btn, w) or (null, null) which is going to be an IEnumerable<(T?, T2?)>
ero
ero2mo ago
well no, because like angius mentioned, nullable reference types are kinda just... sugar for analyzers (T1?, T2?) is still ValueTuple<T1, T2>
Olipro
OliproOP2mo ago
as I have read and understood the documentation, the analyser needs to be able to prove that a type can't be null this is somehow escaping the analyser
ero
ero2mo ago
it's not really surprising. the linq query goes through so many layers, it's impossible to retain that information all OfType does is check value is ReturnType and that check is true for ValueTuple<T1, T2>, ValueTuple<T1?, T2>, ValueTuple<T1, T2?>, and ValueTuple<T1?, T2?>
MODiX
MODiX2mo ago
ero
REPL Result: Success
(C?, D?) tuple = (new C(), new D());
Console.WriteLine(tuple is ValueTuple<C, D>);

class C;
class D;
(C?, D?) tuple = (new C(), new D());
Console.WriteLine(tuple is ValueTuple<C, D>);

class C;
class D;
Console Output
True
True
Compile: 504.577ms | Execution: 32.420ms | React with ❌ to remove this embed.
MODiX
MODiX2mo ago
ero
REPL Result: Success
(C?, D?) tuple = (null, null);
Console.WriteLine(tuple is ValueTuple<C?, D?>);

class C;
class D;
(C?, D?) tuple = (null, null);
Console.WriteLine(tuple is ValueTuple<C?, D?>);

class C;
class D;
Console Output
True
True
Compile: 487.519ms | Execution: 52.578ms | React with ❌ to remove this embed.
Olipro
OliproOP2mo ago
everywhere I've had nullable errors, it's been because it cannot prove it's non-nullable and I've had to change the code so that passes through. if you're saying information is lost through LINQ, that should defacto result in the analyser deciding that it can't prove the types are non-null and emitting an error
MODiX
MODiX2mo ago
ero
REPL Result: Success
(C?, D?) tuple = (null, null);
Console.WriteLine(tuple is ValueTuple<C, D>);

class C;
class D;
(C?, D?) tuple = (null, null);
Console.WriteLine(tuple is ValueTuple<C, D>);

class C;
class D;
Console Output
True
True
Compile: 488.115ms | Execution: 48.817ms | React with ❌ to remove this embed.
ero
ero2mo ago
sure, raise an issue in dotnet/roslyn i'm sure people in that repo can explain it better than i ever could anyway but what you're seeing is a general issue with generic parameters that don't have variance like in classes or structs many things go into this maybe https://github.com/dotnet/roslyn/discussions is best
Olipro
OliproOP2mo ago
I suspect this isn't unique to ValueTuple - I imagine this is applicable to all struct types and it seems this is by design: https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references#structs
Nullable reference types - C#
This article provides an overview of nullable reference types. Learn how the feature provides safety against null reference exceptions, for new and existing projects.
Olipro
OliproOP2mo ago
that makes structs a bit of a turnoff for me if they essentially have inferior nullability safety
Arrays and structs that contain reference types are known pitfalls in nullable references and the static analysis that determines null safety. In both situations, a non-nullable reference might be initialized to null, without generating warnings.
:cough:
ero
ero2mo ago
this specifically isn't about structs. it's about the generic arguments
MODiX
MODiX2mo ago
ero
REPL Result: Success
Tuple<C?, D?> tuple = Tuple.Create<C?, D?>(null, null);
Console.WriteLine(tuple is Tuple<C, D>);

class C;
class D;
Tuple<C?, D?> tuple = Tuple.Create<C?, D?>(null, null);
Console.WriteLine(tuple is Tuple<C, D>);

class C;
class D;
Console Output
True
True
Compile: 490.639ms | Execution: 28.665ms | React with ❌ to remove this embed.
ero
ero2mo ago
this is the class Tuple<T1, T2>
Olipro
OliproOP2mo ago
I see, if I'm understanding this right, whether it's allowed to work or not is dependent on whether or not you define constraints: https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references#generics
Nullable reference types - C#
This article provides an overview of nullable reference types. Learn how the feature provides safety against null reference exceptions, for new and existing projects.
Olipro
OliproOP2mo ago
meaning, if I understand this correctly, that ValueTuple/Tuple have no such constraints
ero
ero2mo ago
lst.Where(btn => btn.Tag is Wanted).Select(btn => (btn, (Wanted)btn.Tag)), maybe? i think you might need nullable suppression in the Select though, still... i've always kinda wanted a Where + Select combination for this, but i'm not sure how you would design it
Want results from more Discord servers?
Add your server