This article will create the first working version of the Cloneable
aspect. Once it is done, it will implement the
Deep Clone pattern as shown below:
1[Cloneable]
2internal class Game
3{
4 public Player Player { get; set; }
5
6 [Child] public GameSettings Settings { get; set; }
7}
1using System;
2
3[Cloneable]
4internal class Game
5: ICloneable
6{
7 public Player Player { get; set; }
8
9 [Child] public GameSettings Settings { get; set; }
10public virtual Game Clone()
11 {
12 var clone = (Game)this.MemberwiseClone();
13 clone.Settings = (this.Settings.Clone());
14 return clone;
15 }
16
17 object ICloneable.Clone()
18 {
19 return Clone();
20 }
21}
1[Cloneable]
2internal class GameSettings
3{
4 public int Level { get; set; }
5 public string World { get; set; }
6}
1using System;
2
3[Cloneable]
4internal class GameSettings
5: ICloneable
6{
7 public int Level { get; set; }
8 public string World { get; set; }
9public virtual GameSettings Clone()
10 {
11 var clone = (GameSettings)this.MemberwiseClone();
12 return clone;
13 }
14
15 object ICloneable.Clone()
16 {
17 return Clone();
18 }
19}
Before we start writing the aspect, we must materialize in C# the concept of a child property. Conceptually, a child
property is a property that points to a reference-type object that needs to be cloned when the parent object is cloned.
Let's decide to mark such properties with the [Child]
custom attribute:
1[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
2public sealed class ChildAttribute : Attribute
3{
4}
Aspect implementation
The whole aspect implementation is here:
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3
4[Inheritable]
5[EditorExperience(SuggestAsLiveTemplate = true)]
6public class CloneableAttribute : TypeAspect
7{
8 public override void BuildAspect(IAspectBuilder<INamedType> builder)
9 {
10 //
11 builder.Advice.ImplementInterface(
12 builder.Target,
13 typeof(ICloneable),
14 OverrideStrategy.Ignore);
15 //
16
17 //
18 builder.Advice.IntroduceMethod(
19 builder.Target,
20 nameof(this.CloneImpl),
21 whenExists: OverrideStrategy.Override,
22 args: new { T = builder.Target },
23 buildMethod: m => m.Name = "Clone");
24 //
25 }
26
27 [InterfaceMember(IsExplicit = true)]
28 private object Clone() => meta.This.Clone();
29
30 [Template]
31 public virtual T CloneImpl<[CompileTime] T>()
32 {
33 // This compile-time variable will receive the expression representing the base call.
34 // If we have a public Clone method, we will use it (this is the chaining pattern). Otherwise,
35 // we will call MemberwiseClone (this is the initialization of the pattern).
36 IExpression baseCall;
37
38 if (meta.Target.Method.IsOverride)
39 {
40 baseCall = (IExpression)meta.Base.Clone();
41 }
42 else
43 {
44 baseCall = (IExpression)meta.This.MemberwiseClone();
45 }
46
47 // Define a local variable of the same type as the target type.
48 var clone = (T)baseCall.Value!;
49
50 // Select cloneable fields.
51 var cloneableFields =
52 meta.Target.Type.FieldsAndProperties.Where(
53 f => f.Attributes.OfAttributeType(typeof(ChildAttribute)).Any());
54
55 foreach (var field in cloneableFields)
56 {
57 // Check if we have a public method 'Clone()' for the type of the field.
58 var fieldType = (INamedType)field.Type;
59
60 field.With(clone).Value = meta.Cast(fieldType, field.Value?.Clone());
61 }
62
63 return clone;
64 }
65}
The BuildAspect
method is the entry point of the aspect.
You can clearly see two steps in this method. We will comment on them independently.
Implementing the interface
The first operation of BuildAspect
is to add the ICloneable method to the current type using
the ImplementInterface method.
11builder.Advice.ImplementInterface(
12 builder.Target,
13 typeof(ICloneable),
14 OverrideStrategy.Ignore);
If the type already implements the ICloneable method, we don't need to do anything, so we are
specifying Ignore
as the OverrideStrategy.
The ImplementInterface method requires the aspect type to include
all
interface members and to annotate them with
the [InterfaceMember] custom attribute.
Our interface implementation calls the public Clone
method we will introduce in the type.
27[InterfaceMember(IsExplicit = true)]
28private object Clone() => meta.This.Clone();
29
For details, see Implementing interfaces.
Note that the code uses the expression meta.This, a compile-time
expression that returns a dynamic
value. Thanks to its dynamic
nature, you can write any run-time expression on its
right side. This code is not verified until all aspects have been executed, so you can call a method that does not exist
yet. For details regarding these techniques, see Generating run-time code
Adding the public method
The second operation of BuildAspect
is to introduce a method named Clone
by
invoking IntroduceMethod.
18builder.Advice.IntroduceMethod(
19 builder.Target,
20 nameof(this.CloneImpl),
21 whenExists: OverrideStrategy.Override,
22 args: new { T = builder.Target },
23 buildMethod: m => m.Name = "Clone");
We set the OverrideStrategy to Override
, indicating that the method should be
overridden if it already exists in the type. The invocation
of IntroduceMethod is more complex than usual for two reasons:
The template method cannot be named
Clone
because it would conflict with the otherClone
method of this aspect, the template for the ICloneable.Clone method. Therefore, we name the template methodCloneImpl
and rename the introduced method using the delegate passed to thebuildMethod
parameter. Hence, the codebuildMethod: m => m.Name = "Clone"
.The
CloneImpl
template, as we will see below, has a compile-time generic parameterT
, whereT
represents the current type. We need to pass the value of theT
parameter in our invocation to the IntroduceMethod method. We pass an anonymous type to theargs
parameter, with the propertyT
set to its desired value:args: new { T = builder.Target }
.
For details, see Introducing members.
Let's now examine the CloneImpl
template:
30[Template]
31public virtual T CloneImpl<[CompileTime] T>()
32{
33 // This compile-time variable will receive the expression representing the base call.
34 // If we have a public Clone method, we will use it (this is the chaining pattern). Otherwise,
35 // we will call MemberwiseClone (this is the initialization of the pattern).
36 IExpression baseCall;
37
38 if (meta.Target.Method.IsOverride)
39 {
40 baseCall = (IExpression)meta.Base.Clone();
41 }
42 else
43 {
44 baseCall = (IExpression)meta.This.MemberwiseClone();
45 }
46
47 // Define a local variable of the same type as the target type.
48 var clone = (T)baseCall.Value!;
49
50 // Select cloneable fields.
51 var cloneableFields =
52 meta.Target.Type.FieldsAndProperties.Where(
53 f => f.Attributes.OfAttributeType(typeof(ChildAttribute)).Any());
54
55 foreach (var field in cloneableFields)
56 {
57 // Check if we have a public method 'Clone()' for the type of the field.
58 var fieldType = (INamedType)field.Type;
59
60 field.With(clone).Value = meta.Cast(fieldType, field.Value?.Clone());
61 }
62
63 return clone;
64}
The first half of the method generates the base call with two possibilities:
- When the method is an override:
var clone = (T) base.Clone();
- Otherwise, when the base type is not deeply clonable:
var clone = (T) this.MemberwiseClone();
MemberwiseClone is a standard method of the object class. It returns a shallow copy of the current object. Using the MemberwiseClone has many benefits:
- It is faster than setting individual fields or properties in C#.
- It works even when the base type is unaware of the Clone pattern.
meta.Base works similarly to the xref:
Metalama.Framework.Aspects.meta.This?text=meta.This> we already used before. It returns a dynamic
value, and anything
you write on its right side becomes a run-time expression, i.e., C# code injected by the template. To convert this code
into a compile-time IExpression object, we cast the dynamic
expression
into IExpression.
The second part of the CloneImpl
template selects all fields and properties annotated with the [Child]
attribute and
generates code according to the pattern clone.Foo = (FooType?) this.Foo?.Clone()
. Fields or properties are represented
as compile-time objects by the IFieldOrProperty interface.
The Value property operates the same kind of magic as meta.This
or meta.Base
above, i.e., a dynamic
property that can be used in run-time code. By default, field.Value
generates
a reference to the field for the current instance (i.e. this.Foo
). To get the field for a different instance (
e.g. clone.Foo
), you must use With.
Summary
In this article, we have created a Cloneable
aspect that performs deep cloning of an object by recursively calling
the Clone
method on child properties. However, we did not validate that the child objects actually have a Clone
method or that child properties are not read-only. We will address this problem the following step.