Сегодня передо мной встала задача сделать полную копию объекта, то есть 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(). Она избавляет нас от монотонной работы копирования полей, но все поля с ссылочными типами придется инициализировать самим.
На этом этапе я вижу по крайней мере две проблемы с типом:
- Ссылочные поля a и lazy объявлены только для чтения (readonly). Поэтому мы можем присвоить им значения только в конструкторе или непосредственно при объявлении поля.
- Конструктор объявляет параметр с таким же именем как и поле класса что вносит некоторую запутанность как в сам код конструктора, так и в код лямбда выражения.
Первую проблему можно решить заменив поля только для чтения на аналогичные свойства объекта.
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; }
Из этого можно сделать такие выводы:
- Старайтесь не использовать одинаковые имена параметров функций и полей/свойств классов или примите соглашение при котором обращение ко внутренним полям происходит только через this.
- Будьте осторожны с использованием замыканий. Обращайте пристальное внимание на то какие ссылки или значения переменных запомнятся во временном объекте.
- Замыкания не должны использовать значения переменных циклов. Но это уже совсем другая исстория.
Автор: vpfau