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