C # делегаты, эталонное время разрешения - PullRequest
7 голосов
/ 17 февраля 2010

У меня простой вопрос о делегатах .net. Скажем, у меня есть что-то вроде этого:

    public void Invoke(Action<T> action)
    {
        Invoke(() => action(this.Value));
    }

    public void Invoke(Action action)
    {
        m_TaskQueue.Enqueue(action);
    }

Первая функция содержит ссылку на this.Value. Во время выполнения, когда вызывается первый метод с универсальным параметром, он как-то предоставит this.Value второму, но как? Это пришло мне в голову:

  • Вызов по значению (структура) - передается текущее значение this.Value, поэтому, если m_TaskQueue выполнит его через 5 минут, значение не будет в своем недавнем состоянии, оно будет быть тем, что было при первой ссылке.
  • Вызов по ссылке (тип ссылки) - тогда самое последнее состояние Value будет ссылаться во время выполнения действия, но если я изменю this.Value на другую ссылку до выполнения действия, оно будет по-прежнему указывать на старую ссылку
  • Вызов по имени (оба) - где this.Value будет оцениваться при вызове действия. Я полагаю, что фактическая реализация будет содержать ссылку на this, а затем оценивать Value на это во время фактического выполнения делегата, поскольку нет вызова по имени.

Я предполагаю, что это будет стиль Call of Name, но не смог найти никакой документации, которая бы задавалась вопросом, является ли это четко определенным поведением. Этот класс похож на Actor в Scala или Erlang, поэтому мне нужно, чтобы он был безопасным для потоков. Я не хочу, чтобы Invoke функция немедленно разыменовывала Value, это будет сделано в безопасном потоке для this объекта m_TaskQueue.

Ответы [ 3 ]

19 голосов
/ 17 февраля 2010

Позвольте мне ответить на ваш вопрос, описав, какой код мы на самом деле генерируем для этого. Я переименую ваш смущенно названный другой метод Invoke; нет необходимости понимать, что здесь происходит.

Предположим, вы сказали

class C<T>
{
  public T Value;
  public void Invoke(Action<T> action) 
  { 
      Frob(() => action(this.Value)); 
  } 
  public void Frob(Action action) 
  {  // whatever
  } 
}

Компилятор генерирует код, как если бы вы на самом деле написали:

class C<T>
{
  public T Value;

  private class CLOSURE
  {
     public Action<T> ACTION;
     public C<T> THIS;
     public void METHOD()
     {
       this.ACTION(this.THIS.Value);
     }
  }

  public void Invoke(Action<T> action) 
  { 
      CLOSURE closure = new CLOSURE();
      closure.THIS = this;
      closure.ACTION = action;
      Frob(new Action(closure.METHOD)); 
  } 
  public void Frob(Action action) 
  {  // whatever
  } 
}

Это отвечает на ваш вопрос?

8 голосов
/ 17 февраля 2010

Делегат хранит ссылку на переменную, а не ее значение. Если вы хотите сохранить текущее значение, тогда (при условии, что это тип значения) вам нужно сделать его локальную копию:

public void Invoke(Action<T> action)
{
    var localValue = this.Value;
    Invoke(() => action(localValue));
}

Если это изменяемый ссылочный тип, вы можете сделать локальный клон / глубокую копию.

4 голосов
/ 17 февраля 2010

Настоящий ключ - помнить, что область видимости является лексической; это то, о чем заботится компилятор. Таким образом, он фиксирует переменные , а не их значения . Являются ли эти значения типами значений или ссылочными типами, это совершенно другой вопрос.

Может быть, поможет немного более экстремальный пример изменения поведения делегата:

var myVariable = "something";
Action a = () => Console.WriteLine(myVariable);
myVariable = "something else entirely"
a();

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

...