Platypus
Platypus

Reputation: 371

Nullable <T> as parameter

I've been trying to mess around with generic types and abstractions for a personnal library project but i'm facing a problem. I found this post that was kinda like what i wanted to do, but I wanted to push it a step further. because i wanted to constrain my function with generic parameter to only few types something like :

public static T Read<T>(T? min, T? max) where T: int, float, double, anything i want
{
}

I know it's impossible in this way, but i'm trying to find some workarounds to achieve something similar

I tried to set to use: T? but I get a message that says that T must not be nullable to be used as a parameter. As you can see from :

where F : ConsoleReadType<T>

I'm basically trying to allow only inherited classes to run.

public abstract class ConsoleReadType<T>
{
    public abstract T Read();
    public abstract T Read(T? min, T? max);
    public virtual F ReadUntilCorrect<F>(Func<F> FunctionToRun, string message = "") /*where F : ConsoleReadType<T>*/
    {
        while (true)
        {
            try
            {
                return FunctionToRun();
            }
            catch (ConsoleInputException)
            {
                if (!string.IsNullOrEmpty(message))
                    ConsoleWrite.Error(message);
            }
        }
    }
}

public class ConsoleReadDouble : ConsoleReadType<double>
{
    public override double Read()
    {
        if (!double.TryParse(Console.ReadLine().Replace(".", ","), out double ret))
        {
            throw new ConsoleInputException();
        }
        return ret;
    }
    public override double Read(double? min, double? max)
    {
        if (!double.TryParse(Console.ReadLine().Replace(".", ","), out double ret))
        {
            throw new ConsoleInputException("invalid input format");
        }
        if (min.HasValue && ret < min || max.HasValue && ret > max)
        {
            throw new ConsoleInputException("input value should be between: " + min + " and " + max);
        }
        return ret;
    }
}

So the main questions are:
1. Is there a way to set nullable T variables in abstract, or is there a better way to achieve what i'm trying to do?
2. Can I allow only certain functions with a where statement ?
3. Is there a way to make these classes static in the end to be used as a helper without having to instanciate them?
4. I'm also interested by any advice you could give me about my code

Thanks a lot.

Upvotes: 1

Views: 126

Answers (2)

NineBerry
NineBerry

Reputation: 28509

You can use the struct constraint (where T: struct) to limit the generic type to value types. You can then use Nullable<T> / T?.

public abstract class ConsoleReadType<T> where T: struct
{
    public abstract T Read();
    public abstract T Read(T? min, T? max);
}

A trick in order to implement ReadUntilCorrect and to be able to use static method calls is to use the concrete inherited class as a type parameter for the abstract base class:

public abstract class ConsoleReadType<T, ConcreteReaderT> 
 where T: struct 
 where ConcreteReaderT: ConsoleReadType<T, ConcreteReaderT>, new()
{
    public abstract T Read();
    public abstract T Read(T? min, T? max);

    public static T ReadUntilCorrect(string message = "") 
    {
        ConcreteReaderT reader = new ConcreteReaderT();
        while (true)
        {
            try
            {
                return reader.Read();
            }
            catch (ConsoleInputException)
            {
                if (!string.IsNullOrEmpty(message))
                    Console.Error.WriteLine(message);
            }
        }
    }
}

// Use the concrete class as its own type parameter so that
// the base class can use the concrete class without knowing it 
// beforehand
public class ConsoleReadDouble : ConsoleReadType<double, ConsoleReadDouble>
{
    public override double Read()
    {
        if (!double.TryParse(Console.ReadLine().Replace(".", ","), out double ret))
        {
            throw new ConsoleInputException();
        }
        return ret;
    }
    public override double Read(double? min, double? max)
    {
        if (!double.TryParse(Console.ReadLine().Replace(".", ","), out double ret))
        {
            throw new ConsoleInputException("invalid input format");
        }
        if (min.HasValue && ret < min || max.HasValue && ret > max)
        {
            throw new ConsoleInputException("input value should be between: " + min + " and " + max);
        }
        return ret;
    }
}

You can then use the concrete classes like this:

class Program
{
    static void Main(string[] args)
    {
        double d = ConsoleReadDouble.ReadUntilCorrect("Please enter a valid number");

    }
}

Upvotes: 2

Camilo Terevinto
Camilo Terevinto

Reputation: 32062

You could use just this:

// add where T: struct so that only structs (int, double, etc) can be used
// allows you to use T? 
public abstract class ConsoleReadType<T> where T: struct
{
    public abstract T Read();
    public abstract T Read(T? min, T? max);
    public virtual T ReadUntilCorrect(Func<T> FunctionToRun, string message = "")
    {
        while (true)
        {
            try
            {
                return FunctionToRun();
            }
            catch (ConsoleInputException)
            {
                if (!string.IsNullOrEmpty(message))
                    ConsoleWrite.Error(message);
            }
        }
    }
}

Is there a way to make these classes static in the end to be used as a helper without having to instanciate them?

Not really, you cannot inherit from a static class, so you'd have to remove the ConsoleReadType<T> class. You could, however, use a Factory approach:

public static class ConsoleReader
{
    public static ConsoleReadType<T> GetReader<T>()
    {
        if (typeof(T) == typeof(double))
        {
            return new ConsoleReadDouble();
        }
        // etc
    }
}

I'm also interested by any advice you could give me about my code

In my opinion, you don't need Read(); at all, Read(T? min, T? max); should be enough. Then, ReadUntilCorrect shouldn't receive a Func<T> but instead call Read. You could do with just:

public abstract class ConsoleReadType<T> where T: struct
{
    public abstract T Read(T? min = null, T? max = null);
    public virtual T ReadUntilCorrect(T? min = null, T? max = null)
    {
        while (true)
        {
            try
            {
                return Read(min, max);
            }
            catch (ConsoleInputException ciex)
            {
                ConsoleWrite.Error(ciex.Message);
            }
        }
    }
}

Upvotes: 4

Related Questions