Your generic type needs to be created with a type argument that must support the members of a particular interface such as the IDisposable
interface.
Use constraints to force the type arguments of a generic type to be of a type that implements one or more particular interfaces:
public class DisposableList<T> : IList<T>
where T : class, IDisposable
{
private List<T> _items = new List<T>();
// Private method that will dispose of items in the list
private void Delete(T item)
{
item.Dispose();
}
// IList<T> Members
public int IndexOf(T item)
{
return (_items.IndexOf(item));
}
public void Insert(int index, T item)
{
_items.Insert(index, item);
}
public T this[int index]
}
get {return (_items[index]);}
set {_items[index] = value;}
}
public void RemoveAt(int index)
}
Delete(this[index]);
_items.RemoveAt(index);
}
// ICollection<T> Members
public void Add(T item)
{
_items.Add(item);
}
public bool Contains(T item)
{
return (_items.Contains(item));
}
public void CopyTo(T[] array, int arrayIndex)
{
_items.CopyTo(array, arrayIndex);
}
public int Count
{
get {return (_items.Count);}
}
public bool IsReadOnly
{
get {return (false);}
}
// IEnumerable<T> Members
public IEnumerator<T> GetEnumerator()
{
return (_items.GetEnumerator());
}
// IEnumerable Members
IEnumerator IEnumerable.GetEnumerator()
{
return (_items.GetEnumerator());
}
// Other members
public void Clear()
{
for (int index = 0; index < _items.Count; index++)
{
Delete(_items[index]);
}
_items.Clear();
}
public bool Remove(T item)
{
int index = _items.IndexOf(item);
if (index >= 0)
{
Delete(_items[index]);
_items.RemoveAt(index);
return (true);
}
else
{
return (false);
}
}
}
This DisposableList
class allows only an object that implements IDisposable
to be passed in as a type argument to this class. The reason for this is that whenever an object is removed from a DisposableList
object, the Dispose
method is always called on that object. This allows you to transparently handle the management of any object stored within this DisposableList
object.
The following code exercises a DisposableList
object:
public static void TestDisposableListCls() { DisposableList<StreamReader> dl = new DisposableList<StreamReader>(); // Create a few test objects. StreamReader tr1 = new StreamReader("c:\\boot.ini"); StreamReader tr2 = new StreamReader("c:\\autoexec.bat"); StreamReader tr3 = new StreamReader("c:\\config.sys"); // Add the test object to the DisposableList. dl.Add(tr1); dl.Insert(0, tr2); dl.Add(tr3); foreach(StreamReader sr in dl) { Console.WriteLine("sr.ReadLine() == " + sr.ReadLine()); } // Call Dispose before any of the disposable objects are // removed from the DisposableList. dl.RemoveAt(0); dl.Remove(tr1); dl.Clear(); }
The where
keyword is used to constrain a type parameter to accept only arguments that satisfy the given constraint. For example, the DisposableList
has the constraint that any type argument T
must implement the IDisposable
interface:
public class DisposableList<T> : IList<T>
where T : IDisposable
This means that the following code will compile successfully:
DisposableList<StreamReader> dl = new DisposableList<StreamReader>();
but the following code will not:
DisposableList<string> dl = new DisposableList<string>();
This is because the string
type does not implement the IDisposable
interface, and the StreamReader
type does.
Other constraints on the type argument are allowed, in addition to requiring one or more specific interfaces to be implemented. You can force a type argument to be inherited from a specific base class, such as the TextReader
class:
public class DisposableList<T> : IList<T>
where T : System.IO.TextReader
, IDisposable
You can also determine if the type argument is narrowed down to only value types or only reference types. The following class declaration is constrained to using only value types:
public class DisposableList<T> : IList<T>
where T : struct
This class declaration is constrained to only reference types:
public class DisposableList<T> : IList<T>
where T : class
In addition, you can also require any type argument to implement a public default constructor:
public class DisposableList<T> : IList<T>where T
: IDisposable,new()
Using constraints allows you to write generic types that accept a narrower set of available type arguments. If the IDisposable
constraint is omitted in the Solution for this recipe, a compile-time error will occur. This is because not all of the types that can be used as the type argument for the DisposableList
class will implement the IDisposable
interface. If you skip this compile-time check, a DisposableList
object may contain objects that do not have a public no-argument Dispose
method. In this case, a runtime exception will occur. Generics and constraints in particular force strict type checking of the class-type arguments and allow you to catch these problems at compile time rather than at runtime.
Get C# 3.0 Cookbook, 3rd Edition now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.