- PVSM.RU - https://www.pvsm.ru -
Сегодня передо мной встала задача сделать полную копию объекта, то есть 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]. Она избавляет нас от монотонной работы копирования полей, но все поля с ссылочными типами придется инициализировать самим.
На этом этапе я вижу по крайней мере две проблемы с типом:
Первую проблему можно решить заменив поля только для чтения на аналогичные свойства объекта.
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; }
Из этого можно сделать такие выводы:
Автор: 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
Нажмите здесь для печати.