Использование кодовых контрактов для определения неизменяемого интерфейса?

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

Ответ 1

Сначала обратите внимание на терминологию в .NET:

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

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

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

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

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

Резюме

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

Ответ 2

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

В приведенном ниже коде описаны различные вещи:

  • Интерфейс 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 для каждого из них только один раз; как только мы его вызываем во второй раз, контракт выдает исключение, потому что возвращаемое значение не соответствует тому, что мы получили в прошлый раз.

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