Вычисляемые значения массива в C# - реактивное программирование
Пример 1
Как мы знаем, есть концепция вычисляемых столбцов в базах данных, где столбец вычисляется на основе значений соседних столбцов.
Проблема в том, что вычисляемые столбцы не могут относиться к другим строкам, кроме текущих.
Пример 2
Затем у нас есть электронные таблицы (например, Excel), где ячейка может иметь формулу. Это похоже на вычисляемый столбец, но более мощный. Формула может быть связана с любой ячейкой (или их множеством) в электронной таблице, а не только с текущей строкой / столбцом, как в RDB.
Эта проблема
Концепция вычисляемых (или автоматически обновляемых значений) великолепна, но как я могу сделать нечто подобное в наборе значений / объектов в C#?
Я хотел бы создать список (или массив) значений, где каждое из них связано с каким-либо другим (или набором) значений в том же списке? Это как значения ячеек электронной таблицы... Измените значение, и связанные с ним изменятся вместе (и все связанные значения поддерева).
Есть ли такая концепция в C#?
Как я собираюсь сделать это самым умным способом? Я знаю, что мог бы иметь LinkedList
объектов, где значения свойств объекта будут относиться к другим объектам в том же списке и будут оцениваться при каждом обращении к значению (оценка по требованию). Это может означать, что несколько (все узлы-предки) будут оценены по пути. Есть ли лучший способ, который работал бы больше как ячейки электронной таблицы, где их сначала оценивают, а затем получают индивидуальный доступ (распространяемая оценка)?
Это, конечно, должно быть применимо и к многомерным массивам.
4 ответа
Эта концепция называется реактивным программированием. В.NET есть нечто, называемое Reactive Extensions, которое позволит вам достичь того, что вы описали. В частности, вам нужно использовать что-то под названием Поведение
Вы можете создать объект со свойством indexer, которое будет содержать ваши вычисления:
class Foo {
public int this[int index] {
get {
return index*2; //Your computation here
}
}
}
или же
class Foo {
public int this[int row,int col] {
get {
return row*col; //Your computation here
}
}
}
В качестве альтернативы, если вы хотите использовать связанный список или что-то подобное, вы можете сохранить объекты "Ячейка" в структуре списка и иметь свойство или метод для этих объектов "Ячейка" для выполнения вычислений:
class Cell {
private int _row;
private int _col;
public Cell(int row,int col) {
_row = row;
_col = col;
}
public int Value {
get {
return _row * _col;
}
}
}
Это идеально подходит для ОО языков, так как ссылки работают. На абстрактном уровне я бы обработал это так:
Создать абстрактный класс, Expression
, это будет базовый тип для всех значений в нашей программе. Что-то вроде:
public abstract class Expression
{
List<Expression> linkedExpressions;
protected Expression lhs; // left hand side, right hand side
protected Expression rhs;
protected Expression(Expression x, Expression y)
{
List<Expression> linkedExpressions = new List<Expression>();
lhs = x;
rhs = y;
// let the expressions know that they have a expression dependant on them
lhs.NotifyExpressionLinked(this);
rhs.NotifyExpressionLinked(this);
}
private void NotifyExpressionLinked(Expression e)
{
if (e != null)
{
linkedExpressions.Add(e);
}
}
private void NotifyExpressionUnlinked(Expression e)
{
if (linkedExpressions.Contains(e)
{
linkedExpressions.Remove(e);
}
}
// this method will notify all subscribed expressions that
// one of the values they are dependant on has changed
private void NotifyExpressionChanged()
{
if (linkedExpressions.Count != 0) // if we're not a leaf node
{
foreach (Expression e in linkedExpressions)
{
e.NotifyExpressionChanged();
}
}
else Evaluate()
// once we're at a point where there are no dependant expressions
// to notify we can start evaluating
}
// if we only want to update the lhs, y will be null, and vice versa
public sealed void UpdateValues(Expression x, Expression y)
{
if (x != null)
{
lhs.NotifyExpressionUnlinked(this);
x.NotifyExpressionLinked(this);
lhs = x;
}
if (y != null)
{
rhs.NotifyExpressionUnlinked(this);
y.NotifyExpressionLinked(this);
rhs = y;
}
NotifyExpressionChanged();
}
public virtual float Evaluate()
{
throw new NotImplementedException(); // we expect child classes to implement this
}
}
Создайте класс для каждого из типов выражений, которые нам понадобятся. В самом низу у вас будет LiteralExpression
, который является просто числом:
public class LiteralExpression : Expression
{
private float value;
public LiteralExpression(float x)
: base(null, null) { } // should not have any linked expressions
public override float Evaluate()
{
return value;
}
}
Об этом классе следует отметить одну вещь - из-за того, как он работает, UpdateValue() не должен использоваться на нем. Вместо этого просто создайте новое выражение LiteralExpression, чтобы заменить его.
Затем вам нужно построить дочерние классы для всех выражений, которые вы захотите (например, вот дополнение):
public class AdditionExpression : Expression
{
public AdditionExpression(Expression x, Expression y)
: base(x, y) { };
public override float Evaluate()
{
return lhs.Evaluate() + rhs.Evaluate();
}
}
Не то, как это весь код, который мы должны писать для каждого выражения - весь тяжелый труд обрабатывается абстрактным классом. В этой программе есть несколько недостатков дизайна - она не обнаружит циклические ссылки и не остановит вас null
значения для выражений (что в случае LiteralExpression требуется), но эти проблемы не должны быть слишком сложными для решения.
Затем все, что нужно сделать, - это реализовать все дочерние классы, которые наследуются от выражения.
Для этого могут быть более совершенные методы, но с точки зрения ОО это хороший пример создания обобщенного абстрактного класса, который реализует общее поведение и имеет множество небольших конкретных реализаций для каждого "аромата" этого класса.
Вы можете создать свой собственный класс массива, который принимает функции вместо значений:
class ComputedArray<T>
{
private Func<T>[] _array;
public T this[int index] { get { return _array[index]( ); } }
public void Set(int index, Func<T> func)
{
_array[index] = func;
}
public ComputedArray( int size )
{
_array = new Func<T>[size];
}
}
Теперь вы можете хранить значения, используя лямбда-выражения:
ComputedArray<int> ar = new ComputedArray<int>( 2 );
ar.Set( 0, ( ) => 2 );
ar.Set( 1, ( ) => ar[0]*2 );
Console.WriteLine( ar[0] );
Console.WriteLine( ar[1] );