- PVSM.RU - https://www.pvsm.ru -

.NET / [Из песочницы] Closures и полное копирование объекта

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

Исходный класс:

class ClassForClone { 	//here are value type fields 	 	public readonly A a; 	public readonly Lazy<string> lazy; 	 	protected void Func1() 	{ 		//to to something; 	} 		 	public ClassForClone(A a) 	{ 		this.a = a; 		lazy = new Lazy<string>(() => 		{ 			// some calculations  			Func1(); 			return a.SomeText; 		}); 	} } 

Воспользуемся функцией побитового копирования полей объекта Object.MemberwiseClone() [1]. Она избавляет нас от монотонной работы копирования полей, но все поля с ссылочными типами придется инициализировать самим.

На этом этапе я вижу по крайней мере две проблемы с типом:

  1. Ссылочные поля a и lazy объявлены только для чтения (readonly [2]). Поэтому мы можем присвоить им значения только в конструкторе или непосредственно при объявлении поля.
  2. Конструктор объявляет параметр с таким же именем как и поле класса что вносит некоторую запутанность как в сам код конструктора, так и в код лямбда выражения.

Первую проблему можно решить заменив поля только для чтения на аналогичные свойства объекта.

public A a { get; private set; } public Lazy<string> lazy { get; private set; } 

Теперь a и lazy можно менять не только внутри конструктора и в момент объявления, но и вообще внутри любой функции нашего класса.

Вторую проблему рассмотрим более подробно. Вернемся к конструктору. Если строчка this.a = a; понятна с первого взгляда, то с лямбда выражением не сразу все очевидно.

Func1 вызовется в контексте текущего экземпляра класса. Но как интерпретировать строчку return a.SomeText? Скорее всего автор подразумевал использование значения поля, а не параметра каким на самом деле является а без ключевого слова this. И, что самое интересное, в исходном коде небыло ошибки, потому что поле a было объявлено только для чтения и его невозможно поменять за рамками конструктора. Как только поле перестает быть только для чтения, лямбда выражение вернет значение поля/свойства SomeText параметра конструктора! А когда дело дойдет до выполнения лябда выражения поле a и параметр а уже могут быть не равны друг другу.
Так как мы заменили поля только для чтения на аналогичные свойства, нам нужно изменить и лямбда выражение:

public ClassForClone(A a) { 	this.a = a; 	lazy = new Lazy<string>(() => 	{ 		// some calculations  		Func1(); 		return this.a.SomeText; 	}); } 

Но гораздо проще ситуация сложилась если бы имена параметров функций не совпадали с именами полей/свойств. Например так:

public ClassForClone(A aParam) { 	a = aParam; 	lazy = new Lazy<string>(() => 	{ 		// some calculations  		Func1(); 		return a.SomeText; 	}); } 

Теперь приступим к функции клонирования. Сразу хочется написать что-то такое:

public object DeepClone() { 	var clone = (ClassForClone) MemberwiseClone(); 	clone.a = new A(); 	clone.lazy = new Lazy<string>(() => 	{ 		Func1(); 		return a.SomeText; 	}); 	return clone; } 

Опять же, нельзя забывать какой объект будет заключен в замыкание. При таком подходе в клоне вызовутся Func1 и a.SomeText оригинального объекта. Поэтому правильная версия такая:

public object DeepClone() { 	var clone = (ClassForClone) MemberwiseClone(); 	clone.a = new A(); 	clone.lazy = new Lazy<string>(() => 	{ 		clone.Func1(); 		return clone.a.SomeText; 	}); 	return clone; } 

Из этого можно сделать такие выводы:

  1. Старайтесь не использовать одинаковые имена параметров функций и полей/свойств классов или примите соглашение при котором обращение ко внутренним полям происходит только через this.
  2. Будьте осторожны с использованием замыканий. Обращайте пристальное внимание на то какие ссылки или значения переменных запомнятся во временном объекте.
  3. Замыкания не должны использовать значения переменных циклов. Но это уже совсем другая исстория [3].

Автор: vpfau


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/net/2235

Ссылки в тексте:

[1] Object.MemberwiseClone(): http://msdn.microsoft.com/en-us/library/system.object.memberwiseclone.aspx

[2] readonly: http://msdn.microsoft.com/en-us/library/acdd6hb7(v=vs.71).aspx

[3] другая исстория: http://stackoverflow.com/questions/271440/c-sharp-captured-variable-in-loop