Общий шаблон нулевого объекта в С#

Мне интересно, существует ли какой-либо подход для реализации шаблона нулевого объекта в С#. Общий нулевой объект является подклассом всех ссылочных типов, как и Nothing в Scala. Кажется,

public class Nothing<T> : T where T : class

Но он не может скомпилировать, и я понятия не имею , как реализовать методы T для обеспечения поведения по умолчанию или исключения исключений. Вот несколько соображений:

  • Использовать отражение?
  • Использовать дерево выражений при создании Nothing<T>? Возможно, это похоже на Moq. И еще вопрос: можно ли использовать mock framework/library в кодах продуктов?
  • Использовать динамические типы?

Я ЗНАЮ, возможно, я должен реализовать конкретный нулевой объект для определенного типа. Мне просто интересно узнать, есть ли какие-либо решения.

Любое предложение? Спасибо.

Ответы

Ответ 1

С помощью дженериков вы не можете определить наследование от T. Если вы намерены использовать if(x is Nothing<Foo>), то это просто не сработает. Не в последнюю очередь, вам нужно будет подумать об абстрактных типах, опечатанных типах и конструкторах, не относящихся к умолчанию. Однако вы можете сделать что-то вроде:

public class Nothing<T> where T : class, new()
{
    public static readonly T Instance = new T();
}

Однако IMO, которая не выполняет большинство ключевых функций нулевого объекта; в частности, вы могли бы легко в конечном итоге с кем-то сделать:

Nothing<Foo>.Instance.SomeProp = "abc";

(возможно, случайно, после прохода объекта 3 уровня вниз)

Честно говоря, я думаю, вам стоит просто проверить null.

Ответ 2

Как насчет этого?

public class Nothing<T> where T : class
{
     public static implicit operator T(Nothing<T> nothing)
     {
          // your logic here
     }
}

Ответ 3

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

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

Ответ 4

Я использую что-то подобное в своих проектах:

public interface IOptional<T> : IEnumerable<T> { }
public interface IMandatory<T> : IEnumerable<T> { }

Два интерфейса, полученных из IEnumerable для совместимости с LINQ

public class Some<T> : IOptional<T>
{
    private readonly IEnumerable<T> _element;
    public Some(T element)
        : this(new T[1] { element })
    {

    }
    public Some()
        : this(new T[0])
    {}
    private Some(T[] element)
    {
        _element = element;
    }
    public IEnumerator<T> GetEnumerator()
    {
        return _element.GetEnumerator();
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

public class Just<T> : IMandatory<T>
{
    private readonly T _element;

    public Just(T element)
    {
        _element = element;
    }
    public IEnumerator<T> GetEnumerator()
    {
        yield return _element;
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

Реализация классов Just и Some

Примечание: реализация этих классов очень похожа, но имеет одно отличие. Класс Просто получен из интерфейса IMandatory и имеет только один конструктор, который гарантирует, что экземпляр класса Just всегда имеет значение внутри.

public static class LinqExtensions
{
    public static IMandatory<TOutput> Match<TInput, TOutput>(
        this IEnumerable<TInput> maybe,
        Func<TInput, TOutput> some, Func<TOutput> nothing)
    {
        if (maybe.Any())
        {
            return new Just<TOutput>(
                        some(
                            maybe.First()
                        )
                    );
        }
        else
        {
            return new Just<TOutput>(
                        nothing()
                    );
        }
    }
    public static T Fold<T>(this IMandatory<T> maybe)
    {
        return maybe.First();
    }
}

Некоторые расширения для практичности

Примечание: метод расширения Сопоставьте две функции и верните IMandatory, после этого метод расширения Fold используйте .First() без каких-либо проверок.

Теперь мы можем использовать полную мощность LINQ и писать код, подобный этому (я имею в виду monads.SelectMany())

var five = new Just<int>(5);
var @null = new Some<int>();

Console.WriteLine(
            five
                .SelectMany(f => @null.Select(n => f * n))
                .Match(
                    some: r => $"Result: {r}",
                    nothing: () => "Ups"
                )
                .Fold()
        );