Open vanillajonathan opened 5 years ago
Thanks @vanillajonathan I think this makes sense. I've marked the issue as api ready for review.
Returning ReadOnlyCollection
means AsReadOnly
will have to make a full copy of the HashSet.
What about a new type ReadOnlySet
than can wrap any ISet<T>
?
Then we will have readonly wrappers for all "ObjectModel" collections: IList, IDictionary, ObservableCollection and ISet.
Feedback - what are you actually trying to do here?
HashSet<T>
already implements IReadOnlyCollection<T>
, so you can cast it directly: IReadOnlyCollection<T> collection = myHashSet;
.
If you want to create a ReadOnlyCollection<T>
, it's one line: ReadOnlyCollection<T> collection = new ReadOnlyCollection<T>(myHashSet.ToArray());
.
I want to create a entity in Entity Framework Core. EF Core internally treats collections as hash sets but they are commonly exposed as ICollection
properties in the DbSet
.
I intend to employ domain-driven design (DDD), so I do not want to expose it as a ICollection
since that would allow the collection to be modified.
Since HashSet
(as you pointed out) implements IReadOnlyCollection<T>
I can expose it as a IReadOnlyCollection
(instead of a ICollection
) but since that is just exposing it as a different interface it is still possible to typecast it back to a ICollection
and modify it.
In your scenario, accessing the Posts
property will return a brand new collection instance every single time. I'd instead recommend using the existing List<T>
and ReadOnlyCollection<T>
types, especially since it seems HashSet<T>
might not be a good fit for the Post
type.
Example:
public class Blog
{
private List<Post> _posts = new List<Post>();
private ReadOnlyCollection<Post> _postsAsReadOnlyCollection = new ReadOnlyCollection<Post>(_posts);
public IReadOnlyCollection<Post> Posts => _postsAsReadOnlyCollection;
public void AddPost(Post post)
{
// business logic here
_posts.Add(post);
}
}
Or, if you really want to use HashSet<Post>
:
public class Blog
{
private HashSet<Post> _posts = new HashSet<Post>();
private ReadOnlyCollection<Post> _cachedROC = null;
public IReadOnlyCollection<Post> Posts => _cachedROC ?? (_cachedROC = new ReadOnlyCollection<T>(_posts.ToArray()));
public void AddPost(Post post)
{
// business logic
_posts.Add(post);
_cachedROC = null; // clear cached value if it exists
}
}
I was under the impression that ToList()
would create a brand new collection instance every single time, and that AsReadOnly
would not do so.
In the first of your examples I would need two private and one public instead of just one public or one private setter. Also it uses List while EF Core internally uses HashSet so the performance may differ.
In the second of your examples, it introduces caching into the business entity.
Both of these are undesirable. Could HashSet
have a AsReadOnly
method, and would that solve this in a clean, efficient manner?
I was under the impression that ToList() would create a brand new collection instance every single time, and that AsReadOnly would not do so.
Yes, it must create a brand new collection every time. The reason for this is that ReadOnlyCollection<T>
requires the underlying collection to implement IList<T>
, which HashSet<T>
does not do on its own. This means that a AsReadOnly()
method which returns a ReadOnlyCollection<T>
necessarily needs to make a copy of the contents of the HashSet<T>
instance.
The reason for this is that
ReadOnlyCollection<T>
requires the underlying collection to implementIList<T>
, whichHashSet<T>
does not do on its own.
This raises the question, should HashSet<T>
implement IList<T>
?
This raises the question, should HashSet
implement IList ?
No. HashSet<T>
is an unordered no-duplicate collection. IList<T>
is an ordered list, potentially with duplicates. They represent two different collection concepts.
This raises the question, should HashSet implement IList?
No.
HashSet<T>
is an unordered no-duplicate collection.IList<T>
is an ordered list, potentially with duplicates. They represent two different collection concepts.
Good point. Could HashSet<T>
have a AsReadOnly
method (like List<T>
does, but without implementing IList<T>
)?
With a List<T>
you can cleanly do things like:
public class Blog
{
private List<Post> _posts;
public IReadOnlyCollection<Post> Posts => _posts?.AsReadOnly();
}
Which you cannot do as cleanly when using a HashSet<T>
instead of a List<T>
.
Which brings us back to the original response on the thread... given your scenario it really would just be easier to have a property that directly casts the HashSet<T>
to an IReadOnlyCollection<T>
.
Since HashSet (as you pointed out) implements IReadOnlyCollection
I can expose it as a IReadOnlyCollection (instead of a ICollection) but since that is just exposing it as a different interface it is still possible to typecast it back to a ICollection and modify it.
So don't cast it back? :)
Which brings us back to the original response on the thread... given your scenario it really would just be easier to have a property that directly casts the
HashSet<T>
to anIReadOnlyCollection<T>
.Since HashSet (as you pointed out) implements IReadOnlyCollection I can expose it as a IReadOnlyCollection (instead of a ICollection) but since that is just exposing it as a different interface it is still possible to typecast it back to a ICollection and modify it.
So don't cast it back? :)
I wouldn't typecast it back to a ICollection<T>
, but if I exposed a IReadOnlyCollection<T>
then I want that to be read-only, but someone using my library could typecast it back a ICollection<T>
and modify it.
With a List<T>
this is much easier to prevent since it has a AsReadOnly
method.
So what is your concrete proposal then? Are you proposing a new type ReadOnlyCollectionWrapper<T>
whose ctor takes an IReadOnlyCollection<T>
instead of an IList<T>
? Is this a scenario that is common enough and that would be burdensome enough for somebody to do themselves that such a type belongs in the Framework proper? Presumably if you need to be unblocked immediately you've already created such a wrapper.
Well I don't know how feasible it is, but I expected the HashSet<T>
to have a AsReadOnly
method, so I would propose the addition of a AsReadOnly
method to the HashSet<T>
.
And what would be the return type of HashSet<T>.AsReadOnly()
? As discussed it can't be ReadOnlyCollection<T>
.
And what would be the return type of
HashSet<T>.AsReadOnly()
? As discussed it can't beReadOnlyCollection<T>
.
You're right, it cannot return a ReadOnlyCollection<T>
since that class implements IList<T>
which HashSet<T>
does not.
Maybe it is weird that ReadOnlyCollection<T>
implements IList<T>
, and that IList<T>
implements ICollection<T>
?
Maybe ReadOnlyCollection<T>
should not have implemented IList<T>
?
Yeah, in hindsight it may have made sense to have two types ReadOnlyList<T>
and ReadOnlyCollection<T>
. That predates my time on this team so I'm not familiar with the history behind why this decision was made. But regardless it's not a decision that can be undone right now. ☹
Maybe things can be fixed for .NET 5?
This would be nice for developers using Entity Framework Core and who wish to implement domain-driven design (DDD).
That's not the type of breaking change we would take in .NET 5.
And what would be the return type of
HashSet<T>.AsReadOnly()
? As discussed it can't beReadOnlyCollection<T>
.
What about ReadOnlySet which can wrap any ISet ? Like https://stackoverflow.com/a/36815316/558098
Assuming we decided to add something like this, I think it should return an IReadOnlySet<T>
instance. This begs the question whether we should be implementing a ReadOnlySet<T>
class that wraps an ISet<T>
instance.
Wouldn’t it be better for this not to be part of the class but just an extension? Especially since HashSet<T>
now implements IReadOnlySet<T>
, this could just be like this:
public static class HashSetExtensions {
public static IReadOnlySet<T> AsReadOnlySet(
this IReadOnlySet<T> readOnlySet) {
return readOnlySet;
}
}
Since a direct cast exists, the rationale for including the above API would be the same as for having Enumerable.AsEnumerable<TSource>()
—to support the use of implicit typing and/or method overload resolution, especially if T
in IReadOnlySet<T>
is big, impractical, or impossible to provide explicitly.
For wrapping ISet<T>
, I’d expect a thin wrapper and the return type to be IReadOnlySet<T>
instead of doing something like returning an instance of ReadOnlyCollection<T>
. Then the reader can see updates made to the ISet<T>
by others (remember, read-only and immutable are very different concepts even though immutable implies unwritable). The biggest issue with this approach would be the specificity/overloads for the appropriate extension methods. Since HashSet<T>
is both ISet<T>
and IReadOnlySet<T>
, the following would create an ambiguity error if merged with the above code:
public static class HashSetExtensions {
public static IReadOnlySet<T> AsReadOnlySet(
this ISet<T> set) {
return new ReadOnlySetAdapter<T>(set);
}
class ReadOnlySetAdapter<T> : IReadOnlySet<T> {
…
}
}
Closing -- would not support exposing an extension method as proposed. It would need to return IReadOnlySet<T>
, but this should only be considered in conjunction with a ReadOnlySet<T>
proposal à la ReadOnlyCollection<T>
.
ReadOnlySet<T>
now exists in .NET 9. We could choose to have an ReadOnlySet<T> AsReadOnly()
if desirable.
I've updated the API proposal to add an extension method in CollectionExtensions
.
The
List<>
class have aAsReadOnly
method to turn the collection into aReadOnlyCollection
, butHashSet<>
does not have any such method.Rationale and usage
Allows Entity Framework Core to expose a collection navigation property as a
IReadOnlyCollection
when backing it with a private backing field declared as aHashSet
.Proposed API