What gets copied
The generator looks at every property and field declared by the type — public or private — and applies these rules in order.
| # | Condition | Result |
|---|---|---|
| 0 | static, const, indexer, write-only, readonly field, [IgnoreClone] |
skipped |
| 1 | required or init-only |
set in the leaf object initializer |
| 2 | get-only collection | contents copied into the existing instance, elements cloned if cloneable |
| 3 | type is cloneable | from.X?.Clone() |
| 4 | settable collection | new instance, elements cloned if cloneable; falls back to a copy constructor |
| 5 | primitive, string, enum, value type, delegate, or registered shallow |
copied directly |
| 5a | type parameter | classified by its constraints, see below |
| 6 | record |
from.X with {} |
| 7 | anything else | copied by reference, with a warning |
Computed properties are skipped
A settable property is only copied if it holds state of its own, i.e. if it is auto-implemented. A property computed from other members is skipped.
[Cloneable]
public partial class Contact
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
// Skipped
public string? FullName
{
get => $"{FirstName} {LastName}";
set => (FirstName, LastName) = Split(value);
}
}
Manually implemented properties are skipped as well, but the private field behind them is copied directly, so no state is lost:
private string? _city;
public string? City { get => _city; set => _city = value; } // to._city = from._city;
Collections
A member counts as a collection if its type implements ICollection<T>.
A get-only collection property is copied into the existing instance rather than replaced:
public List<PhoneNumber> PhoneNumbers { get; } = [];
// foreach (var item in from.PhoneNumbers) to.PhoneNumbers.Add(item?.Clone()!);
A settable collection is tried three ways, in this order:
- Public parameterless constructor: a fresh instance, then the same add-loop as above.
- Constructor taking
IEnumerable<T>:to.Tags = new TagSet(from.Tags); - Neither: copied by reference, with a warning.
Note that only the first of these deep-copies the elements; a copy constructor produces a new collection over the same items.
A settable type that is merely IEnumerable<T> rather than ICollection<T> — Queue<T> and Stack<T>, which have no Add to loop over — also takes the copy-constructor route:
public Queue<Vector2> PathNodes { get; private set; } = new();
// to.PathNodes = new Queue<Vector2>(from.PathNodes);
A nullable one is guarded, because passing null to a copy constructor throws. A get-only member of such a type cannot be filled at all, so it is skipped and the clone keeps whatever its initializer produced; make it settable to have it copied.
Elements are only cloned when their type is cloneable. Strings and other immutable types are copied as-is.
A null check is only emitted when the member's type is annotated nullable; a get-only List<T> is assumed to be non-null, while List<T>? is guarded.
Note that the collection is copied, but the object graph is not deduplicated. Two members referencing the same list produce two independent copies, and an element appearing twice in a collection is cloned twice.
Type parameters
A member typed as a type parameter is classified by what its constraints promise, because the type parameter itself says nothing:
| Constraint | Result |
|---|---|
where T : struct |
copied directly |
| a cloneable type | from.X?.Clone() |
| anything else, or no constraint | copied by reference, with a warning |
Where the constraint is the root of a curiously recurring hierarchy, its Clone() already returns T, so the value is used as-is:
[Cloneable]
public abstract partial class EntityBase<TTemplate>
where TTemplate : EntityTemplateBase<TTemplate>
{
private TTemplate? _template; // to._template = from._template?.Clone();
}
Otherwise Clone() returns the constraint rather than T, so the result is cast back down:
[Cloneable]
public partial class Holder<T> where T : Item
{
public T? Value { get; set; } // to.Value = (T)from.Value?.Clone()!;
}
That cast assumes the hierarchy overrides Clone() to return the concrete type, which generated ones always do. It is only emitted where T is known to be a reference type.
Immutable types
These are always copied by reference, without a warning:
- value types and
enums string- delegates
System.Type,System.Uri,System.Version- anything registered with
[assembly: CloneShallow(typeof(T))]
Everything else that is not cloneable falls through to rule 7. The warning there is limited to types from your own assembly.