C
C#17mo ago
Carsillas

❔ AllOf<>? Something along the same lines as discriminated unions?

Essentially I am trying to write a generic class that is something like this:
public class MyClass<T> {

AllOf<T, MyOtherClass> Value { get; set; }

}

// some function
void Test() {
MyClass<IEquatable> m = new MyClass<IEquatable>();

m.Value = new SomeClassA(); // compile error, does not inherit MyOtherClass
m.Value = new SomeClassB(); // compile error, does not implement IEquatable
m.Value = new SomeClassC(); // success

}


public class SomeClassA : IEquatable { }
public class SomeClassB : MyOtherClass { }
public class SomeClassC : MyOtherClass, IEquatable { }
public class MyClass<T> {

AllOf<T, MyOtherClass> Value { get; set; }

}

// some function
void Test() {
MyClass<IEquatable> m = new MyClass<IEquatable>();

m.Value = new SomeClassA(); // compile error, does not inherit MyOtherClass
m.Value = new SomeClassB(); // compile error, does not implement IEquatable
m.Value = new SomeClassC(); // success

}


public class SomeClassA : IEquatable { }
public class SomeClassB : MyOtherClass { }
public class SomeClassC : MyOtherClass, IEquatable { }
Is there absolutely not way to get this to resolve at compile time? I suspect not, but who knows what kind of crazy ideas people can come up with My current best idea is this, but id like to avoid having to have the setter, but worst case scenario this will probably do the trick
38 Replies
Denis
Denis17mo ago
Just a note, if you wish to have a discriminated union now, why not use the OneOf nuget package?
lycian
lycian17mo ago
Indeed, and OneOf basically does it by keeping a slot and storing the values in a generated class like
class OneOf<T0, T1> {
private readonly T0? _t0;
private readonly T1? _t1;
private readonly int _index;

private OneOf(int index, T0? t0 = default, T1? t1 = default)
{
_index = index;
_t0 = t0;
_t1 = t1;
}

public object? Value => _index switch
{
0 => _t0,
1 => _t1,
_ => throw new InvalidOperationException()
};

public static implicit operator OneOf<T0, T1>(T0 t0) => new(0, t0: t0);
public static implicit operator OneOf<T0, T1>(T1 t1) => new(1, t1: t1);
}
class OneOf<T0, T1> {
private readonly T0? _t0;
private readonly T1? _t1;
private readonly int _index;

private OneOf(int index, T0? t0 = default, T1? t1 = default)
{
_index = index;
_t0 = t0;
_t1 = t1;
}

public object? Value => _index switch
{
0 => _t0,
1 => _t1,
_ => throw new InvalidOperationException()
};

public static implicit operator OneOf<T0, T1>(T0 t0) => new(0, t0: t0);
public static implicit operator OneOf<T0, T1>(T1 t1) => new(1, t1: t1);
}
It definitely provides more, but that's the brunt of usage you're looking for the implicit conversion is the important bit
Carsillas
Carsillas17mo ago
The issue with one of is I don't need it to satisfy one of the types, I need it to satisfy all of the types, the Value should be assignable to both for it to compile
Anton
Anton17mo ago
you can't You can do this with extension methods
Carsillas
Carsillas17mo ago
That's what I figured, unfortunate that it can't be done with a property, the example I posted in the image is probably as close I can get then
MODiX
MODiX17mo ago
AntonC#3545
REPL Result: Failure
public struct AllOf<T1, T2>
{
public T1 t1;
public T2 t2;
}

public static class E1
{
public static void SetValue<T1, T2>(ref this AllOf<T1, T2> a, T1 t1)
{
a.t1 = t1;
}
}

public static class E2
{
public static void SetValue<T1, T2>(ref this AllOf<T1, T2> a, T2 t2)
{
a.t2 = t1;
}
}

var a = new AllOf<int, string>();
a.SetValue(1);
a.SetValue("hello");
public struct AllOf<T1, T2>
{
public T1 t1;
public T2 t2;
}

public static class E1
{
public static void SetValue<T1, T2>(ref this AllOf<T1, T2> a, T1 t1)
{
a.t1 = t1;
}
}

public static class E2
{
public static void SetValue<T1, T2>(ref this AllOf<T1, T2> a, T2 t2)
{
a.t2 = t1;
}
}

var a = new AllOf<int, string>();
a.SetValue(1);
a.SetValue("hello");
Exception: CompilationErrorException
- Extension methods must be defined in a top level static class; E1 is a nested class
- Extension methods must be defined in a top level static class; E2 is a nested class
- 'AllOf<int, string>' does not contain a definition for 'SetValue' and no accessible extension method 'SetValue' accepting a first argument of type 'AllOf<int, string>' could be found (are you missing a using directive or an assembly reference?)
- 'AllOf<int, string>' does not contain a definition for 'SetValue' and no accessible extension method 'SetValue' accepting a first argument of type 'AllOf<int, string>' could be found (are you missing a using directive or an assembly reference?)
- The name 't1' does not exist in the current context
- Extension methods must be defined in a top level static class; E1 is a nested class
- Extension methods must be defined in a top level static class; E2 is a nested class
- 'AllOf<int, string>' does not contain a definition for 'SetValue' and no accessible extension method 'SetValue' accepting a first argument of type 'AllOf<int, string>' could be found (are you missing a using directive or an assembly reference?)
- 'AllOf<int, string>' does not contain a definition for 'SetValue' and no accessible extension method 'SetValue' accepting a first argument of type 'AllOf<int, string>' could be found (are you missing a using directive or an assembly reference?)
- The name 't1' does not exist in the current context
Compile: 656.721ms | Execution: 0.000ms | React with ❌ to remove this embed.
Carsillas
Carsillas17mo ago
I wonder of generating IL directly would support something like this, not that I'll go that route but I wonder if its a language limitation or the some other aspect
Anton
Anton17mo ago
No, not with a property
Carsillas
Carsillas17mo ago
Was hoping that because properties are methods under the hood they could be defined as generic (in the IL)
Anton
Anton17mo ago
I don't think so Just don't use a property for this What's the big deal
Carsillas
Carsillas17mo ago
It's not a huge deal it's just inconsistent with my other API, but maybe it should be? Basically it's a networking library and I can have networked variables like int and string but I wanted a way to replicate references to the network entities themselves, so I planned on having a class that was generic in a way that you could specify the types of classes that were allowed in that property, but using just an interface breaks the serializer because it doesn't know about the network ID that exists on the network entity class
Anton
Anton17mo ago
don't fuss about the syntax too much
Carsillas
Carsillas17mo ago
It will still work if I just make it so the generic must be a subclass of my network entity, just was hoping to be able to use interfaces as well
Anton
Anton17mo ago
if anything, making that an assignment will make it more obfuscated
Carsillas
Carsillas17mo ago
I don't think it's obfuscation to require type safety at compile time
Anton
Anton17mo ago
no I meant a different thing, I forgot the word... cryptic? like in the hard or weird to understand sort of way you just don't normally expect that from a property
Carsillas
Carsillas17mo ago
I feel like it's a reasonable sacrifice to allow compile time enforcement of types, but I do think it's probably the case that a method like I posted in the image will suffice
Anton
Anton17mo ago
extension method are a good solution for this imo
Carsillas
Carsillas17mo ago
How so? I don't think that solves it at all? The extensions you provided above satisfy the usual OneOf use case, not "AllOf" that I'm trying to create here
Anton
Anton17mo ago
it resolves the right overload based on the argument
Carsillas
Carsillas17mo ago
I don't want it to work with types that only implement one of the types, for my case it needs to be assignable to all of the types for it to compile
Anton
Anton17mo ago
oohh I'm sorry I didn't read your post carefully
Carsillas
Carsillas17mo ago
No problem haha That's why I think it's no more "cryptic" than just a regular generic, perhaps less so
Anton
Anton17mo ago
you can do this I think but you'll have to define a class manually for each use case for each different combination
Carsillas
Carsillas17mo ago
Yeah that's what I was hoping to avoid sadly
Anton
Anton17mo ago
it should be fine tho, unless you make kinds of these types via reflection
Carsillas
Carsillas17mo ago
It will work yes but it's pretty stinky for users of the library to have to create a new class for every different kind of variable they want, as if generics don't exist Even if the only user of the library is me haha I think I've established that the worst case scenario for me is using the generic method solution in the image Which isn't too bad
Anton
Anton17mo ago
write a source generator if it matters to you this much
Carsillas
Carsillas17mo ago
Possibly, what would the generated type names be? How would I trigger it? I don't know if a source generator works well here
Anton
Anton17mo ago
it does work well here the generated type names is your own logic it's triggered by the compiler/the build pipeline
Carsillas
Carsillas17mo ago
I mean like what syntax would trigger the source generator? If I write MyClass<ITestInterface> myVariable then I can't swap out the variable type since it's already in the source
Anton
Anton17mo ago
I would make it an attribute or a partial struct with an attribute if it's an attribute, you either make the variable the same type as you wrote in the attribute, or mock it with a base interface you can't use value types in the latter case, obviously but defining a struct an putting on an attribute is a good enough experience
Carsillas
Carsillas17mo ago
That could probably work, hmm
Anton
Anton17mo ago
ah yeah, the base interface won't work, sorry it's the same problem as before well, defining a partial struct, putting an attribute on to it and have it fill up on its own is good enough but I don't think it's worth it in the end it's not that much code you have to write for each type for each combination I mean This is if you want a struct
public struct MyAllOf
{
private BaseType v;
public readonly BaseType GetValue<T>(T t) where T : interface1, interface2 => v;
public BaseType SetValue<T>(T t) where T : interface1, interface2 => v = value;
}
public struct MyAllOf
{
private BaseType v;
public readonly BaseType GetValue<T>(T t) where T : interface1, interface2 => v;
public BaseType SetValue<T>(T t) where T : interface1, interface2 => v = value;
}
well yeah ig you could autogenerate this and have syntax like
[AllOf(typeof(interface1), typeof(interface2))]
public partial struct MyAllOf{}
[AllOf(typeof(interface1), typeof(interface2))]
public partial struct MyAllOf{}
ah, I think I actually know a better solution generate the extension methods instead and have the AllOf be opaque (just wrap a value) you can find which overloads to generate based on usage look up linqgen could be helpful
Anton
Anton17mo ago
GitHub
GitHub - cathei/LinqGen: Alloc-free and fast replacement for Linq, ...
Alloc-free and fast replacement for Linq, with code generation - GitHub - cathei/LinqGen: Alloc-free and fast replacement for Linq, with code generation
Carsillas
Carsillas17mo ago
That's not a bad idea, I worry about how it will work with types outside the current assembly however
Anton
Anton17mo ago
it won't the new extensions will have to be defined in the assembly you use it in you might as well make them internal
Accord
Accord17mo ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.