Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Conditional interface

Tags:

c#

For a project I'm on I'm unfortunately stuck with .NET 2.0 (many of our target machines are still Windows XP), which means no Optional type as Nuget's Optional library requires .NET 3.5.

Fortunately, rolling your own Optional type is pretty easy, but I've encountered one issue.

I would like something like the following:

class Optional<T> : (IComparable<Optional<T>> when T : IComparable<T>)

That is, I want my Optional type to implement Comparable, but only when the underlying type is Comparable.

The above syntax is sadly not valid, but is there a way achieve what I'm looking for?

Indeed this problem isn't confined to Optional, it will apply to any container type one wants to define that can implement it's internal type's interfaces.

I do realise I could do this:

class Optional<T>
class ComparableOptional<T> : Optional<T>, Comparable<ComparableOptional<T>> 
  where T : Comparable<T>

But this seems a bit silly, as then we really have to go down this rabbit hole:

class Optional<T>

class EquatableOptional<T> : 
    Optional<T>, 
    IEquatable<EquatableOptional<T>>
  where T : IEquatable<T>

class ComparableOptional<T> : 
    EquatableOptional<T>,
    IComparable<ComparableOptional<T>>, 
    IEquatable<ComparableOptional<T>>
  where T : IComparable<T>

Furthermore, if T is Enumerable, Optional<T> can also be Enumerable (returning an empty enumerator if there is no value) so then we've got even more classes.

As Enumerable is orthogonal to Equatable and Comparable, we'd really need the following classes:

class Optional
class EquatableOptional
class ComparableOptional
class EnumerableOptional
class EnumerableEquatableOptional
class EnumerableComparableOptional

to cover all cases. Add another orthogonal interface and you've got 12 classes.

Is there a less messy approach that allows me to define interfaces conditionally? This seems like a common issue with any collection.

like image 245
Clinton Avatar asked Mar 27 '18 07:03

Clinton


1 Answers

What you want to do infringes on the intention of how generics work in C#.

You're essentially arguing that type safety should be used as type possibility. Which is against the current C# ideology where you know a type's definition (and which methods and properties it exposes) for a fact.

The correct approach would be to have a second ComparableOptional<T> which derives from Optional<T> but adds an additional constraint:

class ComparableOptional<T> : Optional<T> where T : Comparable<T>

There is no benefit to your suggestion, other than the lazy approach of wanting to mash two different classes together. Even if the language would allow you to do so, I see no discernible benefit to this approach (compared to ComparableOptional<T>) but it does introduce a whole range of runtime errors that you can now encounter.


class Optional<T> : (IComparable<Optional<T>> when T : IComparable<T>) {}

Suppose everything works the way you expect it to.

var optionalPerson = new Optional<Person>() { Person = myPerson };
var optionalPerson2 = new Optional<Person>() { Person = myPerson2 };

int result = optionalPerson.CompareTo(optionalPerson2);

Should this work? In C# currently, it doesn't. But according to you, it should be able to if Person : IComparable<Person>. Your argument should be something like this:

Since the compiler sees me use the type Person : IComparable<Person>, it should be able to deduce that Optional<T> must now implement IComparable<T> and therefore the CompareTo() should be available.

The solidity of your argument rests solely on the fact that you know for a fact (at compile time) that the type you're using implements the needed interface.

But what about this code:

public void DoSomething<T>(Optional<T> opt1, Optional<T> opt2)
{
    int result = opt1.CompareTo(opt2);
}

Should this work? You can't know, since you don't know which type will be used! Compounding the issue even further:

public void DoSomething(string optionalType, object opt1, object opt2)
{
    var castObj = Convert.ChangeType(opt1, Type.GetType(optionalType)));
    var castObj2 = Convert.ChangeType(opt2, Type.GetType(optionalType)));

    int result = castObj .CompareTo(castObj2);
}

This method passes the used type as a string. So now you would expect the compiler to check the value of the string to figure out whether or not the generic type constraint of the type that is represented in the string implements a particular interface.

WHat if that string is retrieved from a database or external web service? Is the compiler now required to have an active database/web connection before it can decide whether your code is valid?

This is running out of hand.

Your likely counterarguments:

As long as I only use this method with types that implement IComparable<T>, the compiler should not throw an error. When I use a type that does not implement IComparable<T>, it should throw an error on the int result line.

That is not intuitive, and is going to lead to developer confusion.

The compiler should always assume that conditional generic type constraints are true.

So how would you handle mutually exclusive conditional generic type constraints, which logically will never both be true?

Welcome to the world of debugging hell. This is bad practice for the same reason that you shouldn't use dynamic over strongly typed approaches: it makes code considerably harder to maintain and develop.

Such an approach requires much more runtime testing to ensure that you haven't made a mistake somewhere that will blow up in your face. And runtime testing is a flawed approach.

like image 175
Flater Avatar answered Nov 10 '22 01:11

Flater