Uses for static generic classes?

2020-02-02 11:39发布

问题:

What are the key uses of a Static Generic Class in C#? When should they be used? What examples best illustrate their usage?

e.g.

public static class Example<T>
{
   public static ...
}

Since you can't define extension methods in them they appear to be somewhat limited in their utility. Web references on the topic are scarce so clearly there aren't a lot of people using them. Here's a couple:-

http://ayende.com/Blog/archive/2005/10/05/StaticGenericClass.aspx

Static Generic Class as Dictionary


Summary of Answers Given

The key issues appear to be "What's the difference between a static generic class with static methods and a non-generic static class with static generic members?"

The decision as to which to use appears to revolve around "Does the class need to store type-specific state internally?"

If there is no need for type-specific internal storage then a static non-generic class with generic static methods appears to be preferable because the calling syntax is nicer and you can define extension methods within it.

回答1:

I use static generic classes for caching reflection-heavy code.

Let's say I need to build an expression tree that instantiates objects. I build it once in the static constructor of the class, compile it to a lambda expression, then cache it in a member of the static class. I often don't make these classes publicly assessable - they are usually helpers for other classes. By caching my expressions in this way, I avoid the need to cache my expressions in some sort of Dictionary<Type, delegate>.

There is an example of this pattern in the BCL. The (DataRow) extension methods Field<T>() and SetField<T>() use the (private) static generic class System.Data.DataRowExtensions+UnboxT<T>. Check it out with Reflector.



回答2:

Making a class static doesn't add any functionality - it's just a convenient check if you intend to use a class without instantiating it. And there are several uses for that...

You can use static generic classes to work around a limitation: C# doesn't permit partial specialization. That means you must either specify all type parameters or none. However, that can be needlessly verbose.

For example:

static class Converter {
    public TOut Convert<TIn, TOut>(TIn x) {...}
}

the previous class doesn't permit type inference since inference doesn't work on return values. However, you also can't specify the return value type without also specifying the input type since you can't partially specialize. Using a (possibly static) generic class, you can specify only one of the two types:

static class ConvertTo<TOut> {
    public TOut Convert<TIn>(TIn x) {...}
}

That way you can let type inference work on the parameter type and specify only the return type.

(Although the above case is conceivable, it doesn't require the generic class to be static, of course).


Secondly, (as Steven first pointed out) a separate static fields exists for each constructed type, and that makes static classes great places to store extra information about types or type combinations. In essence, it's a semi-static hashtable that keys on types.

A semi-static lookup table keying on types sounds a little arcance, but it's actually a very, very useful structure because it allows you to store expensive reflection and code-generation results where they're almost free to look up (cheaper than a dictionary because it gets JIT-ed in and you avoid a call to .GetType()). If you're doing metaprogramming, this is great!

For example, I use this in ValueUtils to store generated hash functions:

//Hash any object:
FieldwiseHasher.Hash(myCustomStructOrClass);

//implementation:
public static class FieldwiseHasher {
    public static int Hash<T>(T val) { return FieldwiseHasher<T>.Instance(val); }
}

public static class FieldwiseHasher<T> {
    public static readonly Func<T, int> Instance = CreateLambda().Compile();
    //...
}

Static generic methods allow type-inference to make usage really easy; static fields on generic classes allow virtually overhead-free storage of (meta)data. It wouldn't surprise me at all if ORM's like Dapper and PetaPoco use techniques like this; but it's also great for (de)serializers. A limitation is that you're getting the low overhead because you're binding to the compile-time type; if the object that's passed is actually an instance of a subclass, you're probably binding to the wrong type - and adding checks to avoid that kind of undermines the benefit of being low-overhead.



回答3:

Static fields of a generic type are specific to the actual type T. This means you can store a type specific cache internally. This could be a reason to create a static generic type. Here is a (rather useless, but informative) example:

public static TypeFactory<T> where T : new()
{
    // There is one slot per T.
    private static readonly object instance = new T();

    public static object GetInstance() {
        return instance;
    }
}

string a = (string)TypeFactory<string>.GetInstance();
int b = (int)TypeFactory<int>.GetInstance();


回答4:

One use of static generic classes that I recently learned was possible is to define a constraint on the class level for the type, then the constraint applies to all the static members of the class, what I also learned is that this is not allowed for static generic classes that define extention methods.

For example if you are going to create a static generic class and you know all the generic types should be IComparable (just an example) then you can do the following.

static class MyClass<T> where T : IComparable
{
  public static void DoSomething(T a, T b)
  {
  }

  public static void DoSomethingElse(T a, T b)
  {
  }
}

Notice that I did not need to apply the constraint to all the member, but just at the class level.



回答5:

The first blog post you mention shows a valid use (as a static repository class for an ActiveRecord implementation):

public static class Repository<T>
{
    public static T[] FindAll { }

    public static T GetById(int id){ }

    public static void Save(T item) { }
}

Or if you wanted to implement different sort methods for Generic arrays:

public static class ArraySort<T>
{
    public static T[] BubbleSort(T[] source) { }

    public static T[] QuickSort(T[] source) { }
}


回答6:

What are the key uses of a Static Generic Class in C#? When should they be used? What examples best illustrate their usage?

I think in general, you should avoid creating type parameters on static classes, otherwise you can't depend on type-inference to make your client code more concise.

To give a concrete example, let's say you're writing a static utility class to handle operations on Lists. You could write the class two ways:

// static class with generics
public static class ListOps<T>
{
    public static List<T> Filter(List<T> list, Func<T, bool> predicate) { ... }
    public static List<U> Map<U>(List<T> list, Func<T, U> convertor) { ... }
    public static U Fold<U>(List<T> list, U seed, Func<T, U> aggregator) { ... }
}

// vanilla static class
public static class ListOps
{
    public static List<T> Filter<T>(List<T> list, Func<T, bool> predicate) { ... }
    public static List<U> Map<T, U>(List<T> list, Func<T, U> convertor) { ... }
    public static U Fold<T, U>(List<T> list, U seed, Func<T, U> aggregator) { ... }
}

But classes are equivalent, but which one is easier to use? Compare:

// generic static class
List<int> numbers = Enumerable.Range(0, 100).ToList();
List<int> evens = ListOps<int>.Filter(numbers, x => x % 2 = 0);
List<string> numberString = ListOps<int>.Map(numbers, x => x.ToString());
int sumOfSquares = ListOps<int>.Fold(numbers, 0, (acc, x) => acc + x*x);

// vanilla static class
List<int> numbers = Enumerable.Range(0, 100).ToList();
List<int> evens = ListOps.Filter(numbers, x => x % 2 = 0);
List<string> numberString = ListOps.Map(numbers, x => x.ToString());
int sumOfSquares = ListOps.Fold(numbers, 0, (acc, x) => acc + b * b);

In my opinion, ListOps<someType> is bulky and clumsy. The definition of the vanilla class is slightly larger, but client code is easier to read -- thanks to type inference.

In the worst case, C# can't infer the types, and you have to specify them by hand. Would you rather write ListOps<A>.Map<B>(...) or ListOps.Map<A, B>(...)? My preference is toward the latter.


The strategy above works particularly well when your static class holds no mutable state, or its mutable state is known at compile time.

If the static class holds mutable state whose type isn't determined at compile time, then you probably have a use case for a static class with generic params. Hopefully these occasions are few and far between, but when it happens, you'll be happy C# supports the functionality.



回答7:

I use these to mock a DbSet when testing against classes that use EntityFramework Async methods.

public static class DatabaseMockSetProvider<TEntity> where TEntity: class
{
    public static DbSet<TEntity> CreateMockedDbSet(IQueryable<TEntity> mockData)
    {
        var mockSet = Mock.Create<DbSet<TEntity>>();
        Mock.Arrange(() => ((IDbAsyncEnumerable<TEntity>)mockSet).GetAsyncEnumerator())
            .Returns(new TestDbAsyncEnumerator<TEntity>(mockData.GetEnumerator()));
        Mock.Arrange(() => ((IQueryable<TEntity>)mockSet).Provider)
            .Returns(new TestDbAsyncQueryProvider<TEntity>(mockData.Provider));
        Mock.Arrange(() => ((IQueryable<TEntity>)mockSet).Expression).Returns(mockData.Expression);
        Mock.Arrange(() => ((IQueryable<TEntity>)mockSet).ElementType).Returns(mockData.ElementType);
        Mock.Arrange(() => ((IQueryable<TEntity>)mockSet).GetEnumerator()).Returns(mockData.GetEnumerator());

        return mockSet;
    }
}

And use them like so in my unit tests - saves a lot of time and can use them for any entity types:

var mockSet = DatabaseMockSetProvider<RecipeEntity>.CreateMockedDbSet(recipes);
        Mock.Arrange(() => context.RecipeEntities).ReturnsCollection(mockSet);


回答8:

You're right: they're not much use. Perhaps there are some rare cases that are exceptions, though. For example, what if the class was a type-specific repository, as in Justin's example, but kept a static collection as a cache? In general, if it contains state, not just methods, there may be a point to this.



回答9:

A static generic class is exactly as useful as any given static class. The difference is that you don't have to use copy-and-paste to create a version of the static class for each type you want it to work on. You make the class generic, and you can "generate" one version for each set of type parameters.