The yield return construct has been introduces by C# 2.0 in order to provide an easy way to create iterators returning IEnumerable<T> generic type instances. Many books and articles treat the construct, so I do not want to go into deep details about how it works. I’d rather like to share a new experience of my own with you, the usefulness of nested iterators. It’s not about that I’ve been discovered the warm water again, the interesting thing is that I even did not recognize the opportunities before.
The base issue arrived when I worked on the C# parser used in the LINQ over C# project. When analyzing the C# source code text there are many operations to repeat on all declared type.
“It must be easy” could you say, let’s use the foreach construct to traverse through all the types. However, it is a bit more than just so simple:
Types con be declared in one or more source files, the same source file can declare zero, one or more types.
Types can be enclosed in namespaces or can be global.
Namespaces can be nested to each other.
Types can declare nested types.
The following figure illustrates the relation among the aforementioned entities:

My first approach to carry out operations (let us assume it is done by the Operation method) was the following:
// --- Carry out the operation on all types
public void OperationOnAllTypes()
{
foreach (SourceFile file in Files)
{
// --- Iterate through the types of the global namespace
foreach (TypeDeclaration type in file.TypeDeclarations)
OperationOnType(type);
// --- Iterate through the namespaces in the file
foreach(NamespaceFragment ns in file.NestedNamespaces)
OperationOnNamespace(ns);
}
}
// --- Carry out the operation on each type in the specified namespace
private void OperationOnNamespace(NamespaceFragment ns)
{
// --- Iterate through the types in this namespace
foreach (TypeDeclaration type in ns.TypeDeclarations)
OperationOnType(type);
// --- Iterate through the nested namespaces
foreach (NamespaceFragment nested in ns.NestedNamespaces)
OperationOnNamespace(nested);
}
private void OperationOnType(TypeDeclaration type)
{
// --- Let’s execute the operation on the type
type.Operation();
// --- Iterate through all the nested types
foreach (TypeDeclaration nestedType in type.NestedTypes)
OperationOnType(nestedType);
}
This code extract is not very long; however, it is not clean enough. If the Operation method would contain arguments those should be passed to the OperationOnAllTypes, OperationOnNamespace and OperationOnType methods. I probably should not tell that in the C# parser I had dozens of operations with arguments.
I started to write down this construct for the third time when I got disturbed and had an uncomfortable feeling. It was not about just typing but rather about the unclearness of the pattern. “Give me those iterators!”--I thought. I refactored the pattern above and here is the result:
public IEnumerable<TypeDeclaration> TypesInSource
{
get
{
foreach (SourceFile file in Files)
foreach (TypeDeclaration type in GetTypesInDeclarationScope(file))
yield return type;
}
}
public IEnumerable<TypeDeclaration> GetTypesInDeclarationScope(
ITypeDeclarationScope scope)
{
// --- Types within the current scope (file or namespace)
foreach (TypeDeclaration type in scope.TypeDeclarations)
foreach (TypeDeclaration toReturn in GetTypesInType(type))
yield return toReturn;
// --- Types in the nested namespaces
foreach (NamespaceFragment nestedNs in scope.NestedNamespaces)
foreach (TypeDeclaration toReturn in GetTypesInDeclarationScope(nestedNs))
yield return toReturn;
}
public IEnumerable<TypeDeclaration> GetTypesInType(TypeDeclaration type)
{
yield return type;
// --- Iterate through the nested types
foreach (TypeDeclaration nestedType in type.NestedTypes)
foreach (TypeDeclaration toReturn in GetTypesInType(nestedType))
yield return toReturn;
}
This source code is not essentially shorter than the original one, it has I huge result: the traversal through the types has been completely separated from the operation carried out on them. Using this pattern it is quite easy to execute the operation on each type:
foreach (TypeDeclaration type in TypesInSource)
{
Operation();
}
Should the Operation have arguments, we would have to write them only once and not carry on through the iteration cycles. Using the nested iterators our code will be clear and if we have many operations to be done with the iteration items the code will be essentially shorter.
Posted
Aug 19 2008, 08:00 AM
by
inovak