Reputation: 6125
We have a set of classes that exclusively use readonly
fields to help ensure that the classes are safe and stable across the scope of the software, especially across threads. Read-only data, as we all know, is a Good Thing™.
In particular, these classes use readonly
to ensure that other developers working in the codebase cannot alter them — even by accident — including within methods in the same class: It's a safety check to keep coders of lesser skill or of lesser codebase knowledge out of trouble, which is why these classes prefer to use readonly
rather than just to have a property with a private set
method. There is no better safety check than the compiler itself telling you "NO".
But there's an interesting Catch-22 that shows up when everything is readonly
, which is that parent/child relationships become impossible if the references are supposed to point both ways. Consider the code below:
public class Parent
{
public readonly IList<Child> Children;
public Parent(IEnumerable<Children> children)
{
Children = Array.AsReadOnly(children.ToArray());
}
}
public class Child
{
public readonly Parent Parent;
public Child(Parent parent)
{
Parent = parent;
}
}
This is legal C# code. It will compile. And it's the right way to model the relationship.
But it's impossible to instantiate either Parent
or Child
correctly: Parent
cannot be instantiated until you have a full collection of children to attach to it, but each Child
cannot be instantiated without a valid Parent
to hang it off of.
(In our case, the data forms trees, but this problem shows up in many other scenarios: Variations on this same problem show up in readonly
doubly-linked lists, readonly
DAGs, readonly
graphs, and so on.)
Typically, I've worked around this Catch-22 using some kind of hack. Options I've used in the past include:
Remove readonly
from Child.Parent
, and use a comment to tell people not to write to that field. This works, but it relies on human trust, rather than on the hammer of the compiler telling people not to do something they shouldn't.
Make Child.Parent
into a property with an internal set
. This works, but allows other methods in the same assembly to potentially change Child.Parent
as well, which violates the design goal of having everything be readonly
.
Make Child.Parent
into a property with a private set
, and add an internal Child.SetParent()
method. This works, but it allows methods on Child
to potentially change Parent
directly, and still provides a loophole for code elsewhere in the same assembly to alter Child.Parent
. And while the Parent()
constructor remains O(n), its constant-time performance is substantially worse.
Make the Parent()
constructor clone each Child
instance using a Child.Clone()
method. This works, but it violates the expectation that the instances you pass in are the instances that Parent
will store. It also can make the Parent()
constructor run in potentially much worse than the expected O(n) time if the clone is a deep clone, which violates expectations for the construction performance of Parent
.
Make the Parent()
constructor take the actual IList
, instead of taking an IEnumerable
and creating a readonly
duplicate. This works, but it trusts that the caller will not use the list after Parent
has been constructed. It also changes the expected behavior of the Parent()
constructor that you can pass in a collection of any type and it will "just work."
Make the Parent()
constructor use reflection to update Child.Parent
. This works, but it's sneaky and "magical", it violates the expectation that Child.Parent
won't change after Child
is constructed, and it's a lot slower than direct writes to the fields.
In short, there's not an easy answer for this problem that I've been able to find. I've used each of the above solutions at various points in the past, but they're all sufficiently distasteful that I'm wondering if anyone has a better answer.
So, in summary: Is there a better solution that you've found to the Catch-22 of having exclusively readonly
cross-class references?
(And yes, I know that in some ways, I'm trying to use technology to solve a people problem, but that's not really the point: The question is whether the cross-class readonly
pattern can be made functional, not whether it should exist in the first place.)
Upvotes: 0
Views: 100
Reputation: 23721
As mentioned in my comment, another possibility which does not require compromising the readonly-ness of your parent/children references is to construct your "Child" objects by way of a factory method passed into the "Parent" object:
public class Parent
{
private readonly Child[] _children;
public Parent(Func<Parent, IEnumerable<Child>> childFactory)
{
_children = childFactory(this).ToArray();
}
}
public class Child
{
private readonly Parent _parent;
public Child(Parent parent)
{
_parent = parent;
}
}
There are many ways to generate the factory passed to the Parent
constructor, e.g. to just create a parent with 5 children you might use something like:
private IEnumerable<Child> CreateChildren(Parent parent)
{
for (var i = 0; i < 5; i++)
{
yield return new Child(parent);
}
}
...
var parent = new Parent(CreateChildren);
...
As was also mentioned in the comments, this mechanism is not without its potential downsides - you must ensure that your parent object is fully initialized before calling the child factory in the constructor, since it may perform actions (call methods, access properties, etc.) on the parent object, and so the parent object must be in a state where this will not result in unexpected behavior. Things become more complicated if someone derives from your parent object since the derived class will never be fully initialised before the child factory is called (since the constructor in the base class is called before the constructor in the derived class).
Your mileage may therefore vary, and it's up to you to determine whether the benefits of this approach outweigh the costs.
Upvotes: 1
Reputation: 127583
What you are really talking about are immutable classes, not just read-only fields. So to solve your problem take a look at what Microsoft's immutable collections do.
What they do is they have "builder classes" which are the only things allowed to break the rules of immutability, it is allowed to do it because the changes it makes are not visible external to the classes till you call ToImmutable()
on it.
You could do similar with a Parent.Builder
that could have a public void AddChild(Foo foo, Bar bar, Baz baz)
which adds a child to the internal state of the parent.
Once everything is added you call public Parent ToImmutalbe()
on the builder and it returns a Parent
object that has all of it's immutable children built up.
Upvotes: 1
Reputation: 32455
I think this is more design problem than language.
Does Child
class really need to know about Parent
?
If so, then what kind of information or functionality it needs. Can it be moved to the Parent
?
If you follow "Tell not ask" rule/principle, then in Parent
class you can call Child
methods and pass Parent
's information as parameters.
If you still stay will current approach then as your Parent
and Child
classes have "circular" dependency which can be resolved by introducing new class which combine them both.
public class Family
{
private readonly Parent _parent;
private readonly ReadOnlyCollection<Child> _childs;
public Family(Parent parent, IEnumerable<Child> childs)
{
_parent = parent;
_childs = new ReadOnlyCollection<Child>(childs.ToList());
}
}
Upvotes: 0