Reputation: 17248
I have the following code:
public interface ISomeObject
{
IList<ISomeObject> Objects { get; }
}
public class SomeObject : ISomeObject
{
public SomeObject()
{
Objects = new List<SomeObject>();
}
public List<SomeObject> Objects
{
get;
set;
}
IList<ISomeObject> ISomeObject.Objects
{
get
{
// What to do here?
// return Objects; // This doesn't work
return Objects.Cast<ISomeObject>().ToList(); // Works, but creates a copy each time.
}
}
SomeObject
has a public property Objects
that returns a List of class type. Clients knowing that class type can use that to do whatever they want. Clients only knowing about ISomeObject
can use the Objects
property only to get an IList<ISomeObject>
. Because it is not allowed to cast List<SomeObject>
to IList<ISomeObject>
(due to the apple and banana issue) I need a way of converting that. The default way, using a Cast.ToList() works, but has the downside that it creates a new List each time the property is evaluated, which may be expensive. Changing ISomeObject.Objects
to return an IEnumerable<ISomeObject>
has the other downside that the client can't use indexing any more (which is quite relevant in my use case). And using Linq's ElementAt() call repeatedly is expensive, when used on an IEnumerable.
Has anybody got an idea on how to avoid either problem?
(of course, making SomeObject
known everywhere is not an option).
Upvotes: 2
Views: 441
Reputation: 111870
You could/should implement a class similar to ReadOnlyCollection<T>
to act as a proxy. Considering that it would be read only, it could be "covariant" (not language-side, but logically, meaning that it could proxy a TDest
that is a subclass/interface of TSource
) and then throw NotSupportedException()
for all the write methods.
Something like this (code untested):
public class CovariantReadOlyList<TSource, TDest> : IList<TDest>, IReadOnlyList<TDest> where TSource : class, TDest
{
private readonly IList<TSource> source;
public CovariantReadOlyList(IList<TSource> source)
{
this.source = source;
}
public TDest this[int index] { get => source[index]; set => throw new NotSupportedException(); }
public int Count => source.Count;
public bool IsReadOnly => true;
public void Add(TDest item) => throw new NotSupportedException();
public void Clear() => throw new NotSupportedException();
public bool Contains(TDest item) => IndexOf(item) != -1;
public void CopyTo(TDest[] array, int arrayIndex)
{
// Using the nuget package System.Runtime.CompilerServices.Unsafe
// source.CopyTo(Unsafe.As<TSource[]>(array), arrayIndex);
// We love to play with fire :-)
foreach (TSource ele in source)
{
array[arrayIndex] = ele;
arrayIndex++;
}
}
public IEnumerator<TDest> GetEnumerator() => ((IEnumerable<TDest>)source).GetEnumerator();
public int IndexOf(TDest item)
{
TSource item2 = item as TSource;
if (ReferenceEquals(item2, null) && !ReferenceEquals(item, null))
{
return -1;
}
return source.IndexOf(item2);
}
public void Insert(int index, TDest item)
{
throw new NotSupportedException();
}
public bool Remove(TDest item)
{
throw new NotSupportedException();
}
public void RemoveAt(int index)
{
throw new NotSupportedException();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
Use it like:
IList<string> strs = new List<string>();
IList<object> objs = new CovariantReadOlyList<string, object>(strs);
Upvotes: 4
Reputation: 1978
You may want try to encapsulate your List<SomeObject>
making it an implementation detail and return IReadOnlyList<SomeObject>
instead. Then SomeObject
to ISomeObject
cast want be unnecessary in interface implementation as well due to IReadOnlyList
variance — you'll be able to return your Objects
as IReadOnlyList<ISomeObject>
.
Then just add some operations to mutate your underlying list like Add
or Remove
to container type if those are required.
Also I should mention that interfaces are not so good for restriction — evil consumer can easily cast your ISomeObject
to SomeObject
and do everything he wants, probably, you should reconsider your design. You'd better stick to such things as immutability and encapsulation for providing usable api. Explicitly use mutable builders then for immutable classes where it's reasonable.
Upvotes: 0
Reputation:
Changing
ISomeObject.Objects
to return anIEnumerable<ISomeObject>
has the other downside that the client can't use indexing any more (which is quite relevant in my use case).
Indexing isn't just supported by the IList<T>
interface, it's also supported by the IReadOnlyList<T>
interface. Because IReadOnlyList<T>
doesn't allow modification, it can be (and is) covariant just like IEnumerable<T>
is.
So, just change the return type to IReadOnlyList<ISomeObject>
and return the original list.
Of course, nothing prevents the caller from casting the result to List<SomeObject>
, but the caller is supposed to have full access to that list anyway, so there is no security risk.
Upvotes: 3