Как использовать nullable вместе с дженериком?

У меня есть вот такой интерфейс

public interface IParse
{
    T TryParse<T>(string line, string pattern);
}

и класс, который его реализует, содержит примерно вот такой метод

public DateTime TryParse<DateTime>(string line, string pattern)
{
    Match match = Regex.Match(line, pattern);
    if (!match.Success)
    {
        throw new Exception();
    }

    DateTime date;
    var success = DateTime.TryParse(match.Value, out date);

    if (!success)
    {
        throw new Exception();
    }

    return date;
}

Я бы хотел иметь возможность возвращать null из метода TryParse, но написать например T? TryParse<T?>(string line); нельзя. Существует ли возможность типизации nullable'ом? К тому же еще возникла проблема - DateTime.TryParse(match.Value, out date); дает ошибку "'DateTime' is a type parameter, which is not valid in the given context", то есть DateTime теперь воспринимается теперь как тип дженерика, а не статический класс и запарсить тоже не выходит.

UPD. Изначально задача примерно такая: есть файл, в котором построчно лежат данные, из которых можно собрать объект. Пусть будет например ДатаРождения, ФИО, Профессия, Зарплата, Стаж. Данные эти обрамлены всякими ненужными деталями, вроде html-тегов, причем разного формата. И мне нужно формировать из них объекты, причем надо сделать возможность конфигурирования структуры объектов без переписывания кода. Например, нужен объект из ДатыРождения, ФИО, Стаж. Или ДатаРождения, ФИО, Профессия. За признак начала новой записи можно взять ДатуРождения.

Соответственно, я планировал решить так: сделать интерфейс с обобщенным методом Parse. Написать несколько классов, каждый из которых реализует метод Parse по-своему, типизируя его типом, подходящим под каждое поле - DateTime, string, string, decimal, int, а еще содержащим регулярку, отсекающую html-шум, характерный для поля. Далее сделать класс-контейнер, в который передать эти парсеры и читать исходные строки, прогоняя каждую через список парсеров. Если парсер может извлечь свои данные из этой строки, он это делает и добавляет результат в конечный объект. Таким образом, если мне понадобится в следующий раз другой список полей, я просто в контейнер передам другие экземпляры парсеров.


Ответы (2 шт):

Автор решения: default locale

Если в интерфейсе объявлен обобщенный (дженерик) метод, то в классе-наследнике его можно реализовать только обобщенным методом. Иначе нельзя, ведь через интерфейс метод можно вызвать с любым параметром:

IParse p = new DateTimeParse();    //класс-наследник
p.TryParse<string>("foo", "bar");  //и что теперь должно произойти?

Лучше будет объявить параметр для всего интерфейса:

public interface IParse<T> 
{
    T TryParse(string line, string pattern);
}

После этого его уже можно будет реализовать с конкретным классом:

public class DateTimeParse : IParse<DateTime?>
{
    public DateTime? TryParse(string line, string pattern)
    {
        var match = Regex.Match(line, pattern);
        //исключения и null по смыслу
        if (!match.Success) return null;
        if (!DateTime.TryParse(match.Value, out var date)) return null;
        return date;
    }
}
→ Ссылка
Автор решения: Alexander Petrov

Предлагаю рассмотреть следующий код.

В интерфейсе добавляем делегат.

public interface IParse
{
    T Parse<T>(string line, string pattern, Func<string, T> func);
}

В классе, реализующем этот интерфейс, используем этот делегат.

public class Parser : IParse
{
    public T Parse<T>(string line, string pattern, Func<string, T> func)
    {
        Match match = Regex.Match(line, pattern);

        if (!match.Success)
        {
            throw new Exception();
        }

        // Здесь может быть выброшено исключение
        var result = func(match.Value);

        return result;
    }
}

Теперь работаем с этим парсером.

var parser = new Parser();

var dateTime = parser.Parse<DateTime>(
    "Конец света: 2020-09-15",
    @"\d{4}-\d{2}-\d{2}",
    s => DateTime.Parse(s)
);

Console.WriteLine(dateTime);


var price = parser.Parse<decimal>(
    "Продам гараж: 100500 р.",
    @"\d+",
    decimal.Parse
);

Console.WriteLine(price);

На мой взгляд, нет смысла использовать конструкцию TryParse в делегате Func. Ведь тогда нужна дополнительная проверка на success и ручной выброс исключения. Можно просто использовать Parse - исключение будет выброшено само. Но можно легко переделать.


Вы пишете, что хотите сделать несколько классов, каждый из которых реализует метод по-своему.

Тогда нужно сам интерфейс делать обобщённым, а не его метод.
Кроме того, я бы вынес задание шаблона (pattern) и делегата в конструктор класса, убрав их из метода. На мой взгляд, так становится проще использовать метод Parse.

public interface IParse<T>
{
    T Parse(string line);
}

public class Parser<T> : IParse<T>
{
    private string _pattern;
    private Func<string, T> _func;

    public Parser(string pattern, Func<string, T> func)
    {
        _pattern = pattern;
        _func = func;
    }

    public T Parse(string line)
    {
        Match match = Regex.Match(line, _pattern);

        if (!match.Success)
        {
            throw new Exception();
        }

        // Здесь может быть выброшено исключение            
        var result = _func(match.Value);

        return result;
    }
}

Теперь создаём и используем экземпляры классов, каждый из которых заточен на парсинг своего типа данных.

var dateTimeParser = new Parser<DateTime>(
    @"\d{4}-\d{2}-\d{2}",
    s => DateTime.Parse(s));

var dateTime = dateTimeParser.Parse(
    "Конец света: 2020-09-15");
Console.WriteLine(dateTime);


var decimalParser = new Parser<decimal>(
    @"\d+",
    decimal.Parse);

var price = decimalParser.Parse(
    "Продам гараж: 100500 р.");
Console.WriteLine(price);


Мне хочется продолжить обсуждение.

Вопрос легко решается возвратом общего типа (можно просто object). Но дальше нужна дополнительная диспетчеризация по внутреннему типу, что усложняет и замедляет код.

Я вижу следующий изъян в текущей реализации с множеством парсеров: каждый из этих парсеров будет проходить по одному и тому же набору данных (строкам текста) - это очень неэффективно.

А какова дальнейшая задача? Извлечённые данные нужно как-то обрабатывать - так? Значит, вместо набора парсеров должен быть набор обработчиков (процессоров) данных. И один парсер.

Этот парсер проходит по тексту, извлекает данные и вызывает конкретный обработчик для этого типа данных.

Возможно, подойдёт паттерн Посетитель (Visitor).

→ Ссылка