Pull to refresh

Closures и полное копирование объекта

Reading time 3 min
Views 10K
Сегодня передо мной встала задача сделать полную копию объекта, то есть 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. Ссылочные поля a и lazy объявлены только для чтения (readonly). Поэтому мы можем присвоить им значения только в конструкторе или непосредственно при объявлении поля.
  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. Замыкания не должны использовать значения переменных циклов. Но это уже совсем другая исстория.
Tags:
Hubs:
+17
Comments 54
Comments Comments 54

Articles