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