Использование Code Contracts для определения неизменного интерфейса? - PullRequest
10 голосов
/ 04 февраля 2012

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

Ответы [ 2 ]

4 голосов
/ 04 февраля 2012

Первое примечание по терминологии в .NET:

  • только для чтения: интерфейс, который у вас есть, не может использоваться для изменения объекта или коллекции
  • неизменяемый: ничего не можетмутировать объект или коллекцию

Теперь вернемся к вашему вопросу.

Все получатели свойств неявно помечены как "Pure" в .NET Code Contracts.Это означает, что чтение из метода получения никогда не должно иметь видимых побочных эффектов.

В строгом смысле, если у вас есть абстрактный интерфейс только со свойствами только для чтения, тогда весь интерфейс считается доступным только для чтения,

Однако, похоже, что вы действительно хотите, чтобы способ пометить интерфейс как неизменяемый и иметь базовые классы, наследующие этот статус.К сожалению, нет никакого способа сделать это, абстрактные интерфейсы могут только добавить функциональность.И лучшее, что могут сделать Code Contracts, - это убедиться, что функциональность была добавлена ​​правильно.

Сводка

Нет, это не поддерживается.

2 голосов
/ 04 февраля 2012

Вот возможное решение в качестве доказательства концепции. С этим связаны различные проблемы, не в последнюю очередь то, что все объекты будут поддерживаться в кеше, и мы используем метод расширения для эффективного подвоха структуры кода, позволяющей нам поддерживать состояние, но это хотя бы демонстрирует, что этот договорный тест возможен.

Код ниже определяет различные вещи:

  • Интерфейс IRuntimeProperty со свойством AlwaysTheSame, которое возвращает целое число. Нам все равно, что это за значение, но хотелось бы, чтобы оно всегда возвращало одно и то же.
  • Статический класс RuntimePropertyExtensions, который определяет метод расширения IsAlwaysTheSame, который использует кэш предыдущих результатов IRuntimeProperty объектов .
  • Класс RuntimePropertyContracts, который вызывает метод расширения для проверки возвращаемого значения из AlwaysTheSame.
  • Класс GoodObject, который реализует AlwaysTheSame так, как нам нравится, поэтому он всегда возвращает одно и то же значение для данного объекта.
  • Класс BadObject, который реализует AlwaysTheSame способом, который нам не нравится, поэтому последовательные вызовы возвращают разные значения.
  • A Main метод для проверки контракта.

Вот код:

using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;

namespace SameValueCodeContracts
{
    [ContractClass(typeof(RuntimePropertyContracts))]
    interface IRuntimeProperty
    {
        int AlwaysTheSame { get; }
    }

    internal static class RuntimePropertyExtensions
    {
        private static Dictionary<IRuntimeProperty, int> cache = new Dictionary<IRuntimeProperty, int>();

        internal static bool IsAlwaysTheSame(this IRuntimeProperty runtime, int newValue)
        {
            Console.WriteLine("in IsAlwaysTheSame for {0} with {1}", runtime, newValue);
            if (cache.ContainsKey(runtime))
            {
                bool result = cache[runtime] == newValue;
                if (!result)
                {
                    Console.WriteLine("*** expected {0} but got {1}", cache[runtime], newValue);
                }
                return result;
            }
            else
            {
                cache[runtime] = newValue;
                Console.WriteLine("cache now contains {0}", cache.Count);
                return true;
            }
        }
    }

    [ContractClassFor(typeof(IRuntimeProperty))]
    internal class RuntimePropertyContracts : IRuntimeProperty
    {
        public int AlwaysTheSame
        {
            get
            {
                Contract.Ensures(this.IsAlwaysTheSame(Contract.Result<int>()));
                return default(int);
            }
        }
    }

    internal class GoodObject : IRuntimeProperty
    {
        private readonly string name;
        private readonly int myConstantValue = (int)DateTime.Now.Ticks;

        public GoodObject(string name)
        {
            this.name = name;
            Console.WriteLine("{0} initialised with {1}", name, myConstantValue);
        }

        public int AlwaysTheSame
        {
            get
            {
                Console.WriteLine("{0} returning {1}", name, myConstantValue);
                return myConstantValue;
            }
        }
    }

    internal class BadObject : IRuntimeProperty
    {
        private readonly string name;
        private int myVaryingValue;

        public BadObject(string name)
        {
            this.name = name;
            Console.WriteLine("{0} initialised with {1}", name, myVaryingValue);
        }

        public int AlwaysTheSame
        {
            get
            {
                Console.WriteLine("{0} returning {1}", name, myVaryingValue);
                return myVaryingValue++;
            }
        }
    }

    internal class Program
    {
        private static void Main(string[] args)
        {
            int value;

            GoodObject good1 = new GoodObject("good1");
            value = good1.AlwaysTheSame;
            value = good1.AlwaysTheSame;
            Console.WriteLine();

            GoodObject good2 = new GoodObject("good2");
            value = good2.AlwaysTheSame;
            value = good2.AlwaysTheSame;
            Console.WriteLine();

            BadObject bad1 = new BadObject("bad1");
            value = bad1.AlwaysTheSame;
            Console.WriteLine();

            BadObject bad2 = new BadObject("bad2");
            value = bad2.AlwaysTheSame;
            Console.WriteLine();

            try
            {
                value = bad1.AlwaysTheSame;
            }
            catch (Exception e)
            {
                Console.WriteLine("Last call caused an exception: {0}", e.Message);
            }
        }
    }
}

Это дает вывод следующим образом:

good1 initialised with -2080305989
good1 returning -2080305989
in IsAlwaysTheSame for SameValueCodeContracts.GoodObject with -2080305989
cache now contains 1
good1 returning -2080305989
in IsAlwaysTheSame for SameValueCodeContracts.GoodObject with -2080305989

good2 initialised with -2080245985
good2 returning -2080245985
in IsAlwaysTheSame for SameValueCodeContracts.GoodObject with -2080245985
cache now contains 2
good2 returning -2080245985
in IsAlwaysTheSame for SameValueCodeContracts.GoodObject with -2080245985

bad1 initialised with 0
bad1 returning 0
in IsAlwaysTheSame for SameValueCodeContracts.BadObject with 0
cache now contains 3

bad2 initialised with 0
bad2 returning 0
in IsAlwaysTheSame for SameValueCodeContracts.BadObject with 0
cache now contains 4

bad1 returning 1
in IsAlwaysTheSame for SameValueCodeContracts.BadObject with 1
*** expected 0 but got 1
Last call caused an exception: Postcondition failed: this.IsAlwaysTheSame(Contract.Result())

Мы можем создать столько GoodObject экземпляров, сколько захотим. Звонок AlwaysTheSame на них всегда удовлетворит контракт.

Напротив, когда мы создаем BadObject экземпляров, мы можем вызвать AlwaysTheSame для каждого только один раз ; как только мы вызываем его во второй раз, контракт вызывает исключение, потому что возвращаемое значение не соответствует тому, что мы получили в прошлый раз.

Как я сказал в начале, я бы с осторожностью использовал этот подход в рабочем коде. Но я согласен с тем, что это полезная вещь, которую нужно указать по контракту, и было бы здорово, если бы была поддержка такой неизменности возвращаемого значения во время выполнения, встроенная в структуру контрактов кода.

...