Generics Open and Closed Constructed Types

Generics open and closed constructed types

In practice the terminology doesn't really matter much - I can't remember the last time I had to worry about it except when trying to write about it.

  • An unbound type has no type arguments specified
  • A constructed type has at least one type argument specified
  • A type parameter is an open type
  • An array type where the element type is open is an open type
  • An open constructed type has at least one type argument which is an open type
  • A closed type is any type which isn't open

(There are further rules for nested types. Consult the C# 3.0 spec section 4.4 for gory details.)

As an example of an open constructed type, consider:

public class NameDictionary<T> : Dictionary<string, T>

The base class of typeof(NameDictionary<>) is:

  • Constructed because it specifies type arguments
  • Open because the second type argument (T) is an open type

The MSDN docs for Type.IsGenericType have quite a useful little table.

Just to reiterate, this is almost entirely unimportant in day to day use.

I'm generally in favour of knowing the correct terminology - particularly for things like "pass by reference" etc - but in this case it really, really doesn't come up very often. I would like to actively discourage you from worrying about it :)

What exactly is an open generic type in .NET?

The C# language defines an open type to be a type that's either a type argument or a generic type defined with unknown type arguments:

All types can be classified as either open types or closed types. An open type is a type that involves type parameters. More specifically:

  • A type parameter defines an open type.
  • An array type is an open type if and only if its element type is an open type.
  • A constructed type is an open type if and only if one or more of its type arguments is an open type. A constructed nested type is an open type if and only if one or more of its type arguments or the type arguments of its containing type(s) is an open type.

A closed type is a type that is not an open type.

Therefore T, List<T>, and Dictionary<string,T>, and Dictionary<T,U> are all open types (T and U are type arguments) whereas List<int> and Dictionary<string,int> are closed types.

There's a related concept: An unbound generic type is a generic type with unspecified type arguments. An unbound type can't be used in expressions other than typeof() and you can't instantiate it or call its methods. For instance, List<> and Dictionary<,> are unbound types.

To clarify the subtle distinction between an open type and an unbound type:

class Program {
static void Main() { Test<int>(); }
static void Test<T>() {
Console.WriteLine(typeof(List<T>)); // Print out the type name
}
}

If you run this snippet, it'll print out

System.Collections.Generic.List`1[System.Int32]

which is the CLR name for List<int>. It's clear at runtime that the type argument is System.Int32. This makes List<T> a bound open type.

At runtime, you can use reflection to bind type arguments to unspecified type parameters of unbound generic types with the Type.MakeGenericType method:

Type unboundGenericList = typeof(List<>);
Type listOfInt = unboundGenericList.MakeGenericType(typeof(int));
if (listOfInt == typeof(List<int>))
Console.WriteLine("Constructed a List<int> type.");

You can check whether a type is an unbound generic type (generic type definition) from which you can construct bound types with the Type.IsGenericTypeDefinition property:

Console.WriteLine(typeof(Dictionary<,>).IsGenericTypeDefinition); // True
Console.WriteLine(typeof(Dictionary<int,int>).IsGenericTypeDefinition); // False

To get the unbound type from a constructed type at runtime, you can use the Type.GetGenericTypeDefinition method.

Type listOfInt = typeof(List<int>);
Type list = listOfInt.GetGenericTypeDefinition(); // == typeof(List<>)

Note that for a generic type, you can either have a completely unbound type definition, or a completely bound definition. You can't bind some type parameters and leave others unbound. For instance, you can't have Dictionary<int,> or Dictionary<,string>.

C# Language: generics, open/closed, bound/unbound, constructed

These are examples of unbound generic types:

  • List<>
  • Dictionary<,>

They can be used with typeof, i.e., the following are valid expressions:

  • typeof(List<>)
  • typeof(Dictionary<,>)

That should answer your question 2. With respect to question 1, note that type arguments can be constructed types or type parameters. Thus, your list should be updated as follows:

public class MyClass<T, U> {  // declares the type parameters T and U

// all of these are
// - generic,
// - constructed (since two type arguments are supplied), and
// - bound (since they are constructed):

private Dictionary<T, U> var1; // open (since T and U are type parameters)
private Dictionary<T, int> var2; // open (since T is a type parameter)
private Dictionary<int, int> var3; // closed
}

.NET generics terminology - open/closed, unbound/constructed

From the language specification:

4.4 Constructed types

A generic type declaration, by itself,
denotes an unbound generic type that
is used as a “blueprint” to form many
different types, by way of applying
type arguments. The type arguments are
written within angle brackets (< and> )
immediately following the name of the generic type. A type that includes
at least one type argument is called a
constructed type. A constructed type
can be used in most places in the
language in which a type name can
appear. An unbound generic type can
only be used within a
typeof-expression (§7.6.11).
[...]

4.4.2 Open and closed types

All types can be classified as either
open types or closed types. An open
type is a type that involves type
parameters. More specifically:

• A
type parameter defines an open type.

• An array type is an open type if and
only if its element type is an open
type.

• A constructed type is an open
type if and only if one or more of its
type arguments is an open type. A
constructed nested type is an open
type if and only if one or more of its
type arguments or the type arguments
of its containing type(s) is an open
type.

A closed type is a type that is
not an open type.
[...]

4.4.3 Bound and unbound types

The term unbound type refers to a non-generic
type or an unbound generic type. The
term bound type refers to a
non-generic type or a constructed
type. An unbound type refers to the
entity declared by a type declaration.
An unbound generic type is not itself
a type, and cannot be used as the type
of a variable, argument or return
value, or as a base type. The only
construct in which an unbound generic
type can be referenced is the typeof
expression (§7.6.11).


Here's an example I thought of:

// Foo<T> is an unbound generic type.
class Foo<T> { .. }

// Bar<K> is an unbound generic type.
// Its base-class Foo<K> is a constructed, open generic type.
class Bar<K> : Foo<K> { .. }

// IntFoo is not a generic type.
// Its base-class Foo<int> is a constructed, closed generic type.
class IntFoo : Foo<int> { .. }

And here's an attempt to tie that in with the reflection API, using the relevant properties: IsGenericType, IsGenericTypeDefinition and ContainsGenericParameters

(These tests are not 100% predictive of each "kind" as per the language spec).

+----------+---------------------+-----------+--------------+-------------------+
| Name | Kind | IsGenType | IsGenTypeDef | ContainsGenParams |
+----------+---------------------+-----------+--------------+-------------------+
| Foo<> | Unbound | TRUE | TRUE | TRUE |
| Foo<>* | Constructed, open | TRUE | FALSE | TRUE |
| Foo<int> | Constructed, closed | TRUE | FALSE | FALSE |
| IntFoo | Not generic | FALSE | FALSE | FALSE |
+----------+---------------------+-----------+--------------+-------------------+
* = Bar<>'s base type.

C#: Construct Type Constrained Half-Closed Generic Implementations of Open Generic Interface via Open Generic Method/Class

It helps to write the problem out, and as it turns out, it was easier than I had originally anticipated. @CRAGIN's answer gave me the final missing piece (as far as...oh, yeah, we can cast to interfaces in C#).

In case anybody from the future stumbles along...

public static IRepository<T> BuildRepository<T>(params object[] constructor_arguments)
where T : class
{
if (typeof(T) == typeof(SqlServerDbModel))
{
return (IRepository<T>)Activator.CreateInstance(typeof(SqlServerDbRepository<>).MakeGenericType(typeof(T)), constructor_arguments);
}
else if (typeof(T) == typeof(DbModel))
{
return (IRepository<T>)Activator.CreateInstance(typeof(BaseRepository<>).MakeGenericType(typeof(T)), constructor_arguments);
}
//... else throw error or just return default...
}

I needed to use the Activator.CreateInstance API to make the object and then just cast it back to the correct type. I wish there was a way to do this in Castle Windsor "natively" without resorting to a custom factory / reflection.

List closed types that runtime has created from open generic types

As far as I can understand in this case Foo<T> is an open unbound generic type, so at runtime the CLR will use it as a blueprint/skeleton to construct and close a generic type with the type parameter type specified (Foo<int>, Foo<object>, etc.). So basically Foo<int> is a runtime constructed implementation of the Foo<T> skeleton.

Now, at run-time you can get the type of Foo<int> either by using typeof(Foo<int>) or typeof(Foo<>).MakeGenericType(new[] { typeof(int) }) and it's not the same Type and it wouldn't make sense for it to be. But look closer and you will see that both typeof(Foo<T>) and typeof(Foo<int>) share the same metadata token and GUID.

Another interesting thing is that typeof(Foo<int>).Assembly will be what you would expect, but as you've noticed already you can't get that type from the Assembly.

That's because Foo<int> is not defined in the assembly (you can check the assembly metadata with Reflector/ILSpy). At run-time the CLR will create ("construct") a specialized ("closed") version of the Foo<T> for Foo<int> (so - constructed closed type of an unbounded open generic type definition) and "give" it a Type. So unless the CLR exposes directly somehow the list of closed generic types it generates at run-time you are out of luck.

Also here is a snippet that might confirm what I am saying:

Even though each construction of a generic type, such as Node< Form >
and Node< String >, has its own distinct type identity, the CLR is able
to reuse much of the actual JIT-compiled code between the type
instantiations. This drastically reduces code bloat and is possible
because the various instantiations of a generic type are expanded at
run time. All that exists of a constructed type at compile time is a
type reference. When assemblies A and B both reference a generic type
defined in a third assembly, their constructed types are expanded at
run time. This means that, in addition to sharing CLR type-identities
(when appropriate), type instantiations from assemblies A and B also
share run-time resources such as native code and expanded metadata.

http://msdn.microsoft.com/en-us/magazine/cc163683.aspx

Why would a closed generic type not be returned by Assembly.GetTypes()?

Remember that reflection is just querying metadata, so any information contained within it is purely compile-type information. The fact that you have an instance of TabularList<SomeType>) does not change the metadata contained within the assembly that defines it.

A closed generic type is not defined within either the assembly that defined the open generic type, nor the assembly that creates that particular closed type.

Would you expect to find metadata for every possible closed definition of List<T> within mscorlib? Would you expect to find it in an assembly that create a List<int> variable?

Note that it does work the other way - if you call

Assembly a = Assembly.GetAssembly(typeof(List<int>));

you get the Assembly

mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

So you might be able to reverse your logic - rather than searching for all closed types within an assembly, look for the assembly of the closed type to see if it's "supported".



Related Topics



Leave a reply



Submit