Constraints on type parameters: interface vs. abstract class
Creating a simple list (UniqueList) of items with a Name property that mu开发者_如何学编程st only contains unique items (defined as having different Names). The constraint on the UniqueList type can be an interface:
interface INamed
{
string Name { get;}
}
or an abstract class:
public abstract class NamedItem
{
public abstract string Name { get; }
}
So the UniqueList can be:
class UniqueList<T> : List<T> where T : INamed
or
class UniqueList<T> : List<T> where T : NamedItem
The class function, AddUnique:
public T AddUnique(T item)
{
T result = Find(x => x.Name == item.Name);
if (result == default(T))
{
Add(item);
return item;
}
return result;
}
If the class type constraint is based on the interface, compiling results in
Error: Operator '==' cannot be applied to operands of type 'T' and 'T'
at the line
if (result == default(T))
All is well if I base UniqueList on the abstract class. Any thoughts?
That's because the interface can be applied to a struct which is a value type. To make it work with the interface extend the constraint like this:
class UniqueList<T> : List<T> where T : INamed, class
That will make sure you won't be able to pass a struct as T
and hence default(T)
will evaluate to null
which is what you expect.
Also, I'd recommend generalizing your UniqueList
a bit, allowing for different types of unique keys:
interface IUnique<TKey>
{
TKey UniqueKey { get;}
}
class UniqueList<TItem,Tkey> : List<TItem> where TItem : IUnique<TKey>, class
Then the INamed
interface can be easily declared as:
interface INamed : IUnique<string>
{
string Name { get;}
}
UniqueKey
or Name
would be implmeneted explicitly in the implementing class to prevent unnecessary (duplicate in fact) public class members.
I don't understand why AddUnique
has to use Find
and compare with default
, can you not use Count
and compare with 0
?
if Count(x => x.Name == item.Name) = 0 {
....
UniqueList<T>
seems like a HashSet
created with a IEqualityComparer
that compares T.Name
I suggest you implement IEquatable<>
and leave the comparison logic to the class. Also do not use ==
for reference types (strings
) as it checks if it is the same object, not if they are equal. To echo others, I suggest you use a Dictionary<>
to check for unique items, or just use the existing KeyedCollection<string, INamed>
which keeps an indexed list as well as a dictionary.
public interface INamed : IEquatable<INamed>
{
string Name { get;}
}
public abstract class NamedItem : INamed
{
public abstract string Name { get; }
public bool Equals(INamed other)
{
if(other==null) return false;
return Name.Equals(other.Name);
}
}
public class UniqueList<T> : List<T>
where T : INamed
{
public T AddUnique(T item)
{
int index = FindIndex((x) => item.Equals(x));
if (index < 0)
{
Add(item);
return item;
}
else
{
return this[index];
}
}
}
Yes. Using the abstract class gives you the System.Object implementation of the == operator, while using the interface does not guarantee to the compiler that the type will support the == operator, since it could be a struct with no implementation. If you add the class
constraint to your type parameter, it should compile.
The interface doesn't provide an implementation for Equals
, so you can't compare the items.
By the way, you seem to be reimplementing HashSet
, consider using the official provided class instead.
Here is a 2nd answer using the KeyedCollection<>
base class.
class Program
{
static void Main(string[] args)
{
UniqueList<IntValue> list = new UniqueList<IntValue>();
list.Add(new IntValue("Smile", 100));
list.Add(new IntValue("Frown", 101));
list.Add(new IntValue("Smile", 102)); // Error, key exists already
int x = list["Smile"].Value;
string frown = list[1].Name;
}
}
public interface INamed : IEquatable<INamed>
{
string Name { get;}
}
public abstract class NamedItem : INamed
{
public abstract string Name { get; }
public bool Equals(INamed other)
{
if(other==null) return false;
return Name.Equals(other.Name);
}
}
public class IntValue : NamedItem
{
string name;
int value;
public IntValue(string name, int value)
{
this.name = name;
this.value = value;
}
public override string Name { get { return name; } }
public int Value { get { return value; } }
}
public class UniqueList<T> : KeyedCollection<string, T>
where T : INamed
{
protected override string GetKeyForItem(T item)
{
return item.Name;
}
}
I will propose a solution using "duck typing". Duck typing in this instance means "If it has a string property called Name, then it is useable." Duck typing doesn't depend on interfaces or abstract base classes.
I'm deriving the collection class from KeyedCollection
because this class already offers key support, but other classes, including List<T>
are possible.
class NamedItemCollection<T> : KeyedCollection<string, T> {
private static readonly Func<T, string> keyProvider;
static NamedItemCollection() {
var x = Expression.Parameter(typeof(T), "x");
var expr = Expression.Lambda<Func<T, string>>(
Expression.Property(x, "Name"),
x);
keyProvider = expr.Compile();
}
protected override string GetKeyForItem(T item) {
return keyProvider(item);
}
}
The first time the class is used with a specific type parameter, we use dynamic code generation to compile a small method. This method reads the name property in a typesafe manner - no matter the container type!
public abstract class NamedItem { public abstract string Name { get; } }
struct Thing { public string Name { get; set; } }
var namedItems1 = new NamedItemCollection<NamedItem>();
var namedItems2 = new NamedItemCollection<Thing>();
var namedItems3 = new NamedItemCollection<Type>();
The suggestions to add a constraint to your T are good ones, because the problem as it stands is -- as others have mentioned -- that your T is not guaranteed to implement the == operator. It will, however, implement the Equals method, though, so you could use it instead...this is handy if you don't want to necessarily contrain your generics to either a exclusively reference- or value-based implementations.
After Blindy's comments, there is a caveat. While Equals is legitimate on all object types, it's not legitimate if your source object is a null (i.e. null.Equals(...) will throw a run-time error), and in the case where T is a class, its default will be null, so you would need to account for that situation before trying to invoke the Equals on a null reference.
Here's my code, which uses both class an structure implementation:
public interface INamed
{
string Name { get; set; }
}
public class Foo<T>
: List<T>
where T : INamed
{
public bool IsUnique(T item)
{
T result = Find(x => x.Name == item.Name);
if (result == null || result.Equals(default(T)))
return true;
return false;
}
}
public class BarClass : INamed
{
public string Name { get; set; }
}
public struct BarStruct : INamed
{
public string Name { get; set; }
}
[STAThread]
static void Main()
{
BarClass bc = new BarClass { Name = "test" };
Foo<BarClass> fc = new Foo<BarClass>();
fc.IsUnique(bc);
BarStruct bs = new BarStruct { Name = "test" };
Foo<BarStruct> fs = new Foo<BarStruct>();
fs.IsUnique(bs);
}
Have you tried defining your interface somewhere along the lines of this?
interface INamed : IEqualityComparer<T>
{
string Name { get;}
}
And then in your method do
public T AddUnique(T item)
{
T result = Find(x => x.Name == item.Name);
if (result.Equals(default(T)))
{
Add(item);
return item;
}
return result;
}
NOTE: code above is not tested, may need tweaking
精彩评论