Array, Collection, IEnumerable<T> Usage Guidelines
Please read this first: About Dave’s ‘unofficial’ Framework Design Guidelines.
þ DO treat a null reference (Nothing in Visual Basic and nullptr in C++/CLI) and an empty collection or IEnumerable<T> as equivalent.
For example, the following constructor treats both a null an empty IEnumerable<string> instance similar; they both result in an empty StringCollection.
public class StringCollection : Collection<string>
{
public StringCollection(IEnumerable<string> items)
{
if (items != null) // Correct
{
foreach (string item in items)
{
Add(item);
}
}
}
}
ý DO NOT return a null reference (Nothing in Visual Basic and nullptr in C++/CLI) from an array, collection or IEnumerable<T> property or method; return an empty instance instead.
Similar to string properties, consumers of your class expect to be to enumerate over the returned enumerable instance without also needing to check for null.
For example, in the following code, performing a foreach over the returned Collection<T> from Directory.GetFiles should never cause a NullReferenceException to be thrown.
void PrintDirectory(string path)
{
Collection<string> fileNames = Directory.GetFiles(path);
foreach (string fileName in fileNames)
{
Console.WriteLine(fileName);
}
}
þ DO throw ArgumentException if an element in collection or IEnumerable<T> argument contains a null reference (Nothing in Visual Basic and nullptr in C++/CLI) and null is not a valid value for an element. Do not filter or skip over these values.
For example, the following method correctly throws ArgumentException when the passed types IEnumerable<Type> instance contains a null element.
public void RegisterTypes(params Type[] types)
{
if (types == null)
throw new ArgumentNullException("types");
if (types.Length == 0)
throw new ArgumentException("'types' cannot be empty.");
foreach (string item in items)
{
if (item == null)
throw new ArgumentException("'items' cannot contain a null element", "items"); // Correct
[...]
}
}
þ CONSIDER providing an params array based overload in addition to an IEnumerable<T> based overload so that user can pass elements without having to explicitly create a temporary array.
For example, the following class has two constructors, one taking an array, and one taking an IEnumerable<T>.
public class TypeCollection : Collection<string>
{
public TypeCollection(params Type[] types) // Correct
: this((IEnumerable<Type>)types)
{
}
public TypeCollection(IEnumerable<Type> types)
{
[...]
}
}
þ CONSIDER providing an IEnumerable<T> overload in addition to an array based overload. This allows users to pass arguments typed as other enumerable sources other than arrays, such as List<T>.
ý AVOID looping over IEnumerable<T> arguments multiple times. IEnumerable<T> instances returned from LINQ-based and yield generated methods are not cached underneath, which could cause work done in the enumerator to be repeated multiple times if an argument is iterated more than once. This could lead to unexpected behavior and performance problems.
For example, the following shows the pitfalls of looping over IEnumerable<T> instance multiple times.
public void DeleteFiles(IEnumerable<string> fileNames)
{
if (fileNames == null)
throw new ArgumentNullException("fileNames");
if (fileNames.Count() == 0) // 1st Loop
throw new ArgumentException("'fileNames' cannot be empty.");
foreach (string fileName in fileName) // 2nd Loop
{
[...]
}
}
This could cause a performance problem given the method as follows:
void DeleteDocumentsWithBackups(string path)
{
string[] fileNames = Directory.GetFiles(path, ".doc");
// Get every document that already has a backup
IEnumerable<string> filesToDelete = fileNames.Select(fileName => File.Exists(fileName + ".bak"));
DeleteFiles(filesToDelete);
}
The lambda expression within the Enumerable.Select extension method will be executed twice for every file name found in the directory; once for Count and once for the foreach statement.
þ DO make a copy of an array or IEnumerable<T> argument if outside modification is not expected. As arrays and IEnumerable<T> instances can be modified after they have been passed to a method, make a clone of the argument if storing it and you want to prevent outside manipulation.
The following example uses the LINQ method, Enumerable.ToArray, to make a copy of the types argument.
public class TypeCollection : ICollection<Type>
{
private readonly Type[] _types;
public TypeCollection(params Type[] types)
: this((IEnumerable<Type>)types)
{
}
public TypeCollection(IEnumerable<Type> types)
{
if (types != null)
{
// Make a copy
_types = types.ToArray(); // Correct
}
}
}
Related Framework Design Guidelines Section: 8.3 Collections