36

Is it better to use List<string> in type annotations or StringList where StringList

class StringList : List<String> { /* no further code!*/ }

I ran into several of these in Irony.

9
  • If you don't inherit from generic types, then why are they there? Dec 17, 2014 at 15:08
  • 7
    @Mawg They can be available only for instantiation. Dec 17, 2014 at 15:24
  • Produced a lot of code like this when upgrading code from .NET 1 style collections.
    – Joshua
    Dec 17, 2014 at 23:02
  • 4
    Yuck. No, seriously. What's the semantic difference between StringList and List<string>? There is none, yet you've (the generic "you") managed to add yet another detail to the code base that is unique only to your project and means nothing to anyone else until after they've studied the code. Why mask the name of a list of strings just to keep calling it a list of strings? On the other hand, if you wanted to do BagOBagels : List<string> I think that makes more sense. You can iterate it as IEnumerable, external consumers don't care about the implementation--goodness all 'round. Dec 18, 2014 at 5:59
  • 1
    XAML has an issue where you can't use generics and you needed to make a type like this to populate a list Dec 18, 2014 at 5:59

9 Answers 9

28

There is another reason why you may want to inherit from a generic type.

Microsoft recommend avoiding nesting generic types in method signatures.

It is a good idea then, to create business-domain named types for some of your generics. Instead of having a IEnumerable<IDictionary<string, MyClass>>, create a type MyDictionary : IDictionary<string, MyClass> and then your member becomes an IEnumerable<MyDictionary>, which is much more readable. It also allows you to use MyDictionary in a number of places, increasing the explicitness of your code.

This may not sound like a huge benefit, but in real-world business code I've seen generics that will make your hair stand on end. Things like List<Tuple<string, Dictionary<int, List<Dictionary<string, List<double>>>>>. The meaning of that generic is in no way obvious. Yet, if it were constructed with a series of explicitly created types the intention of the original developer would likely be a lot clearer. Further still, if that generic type needed to change for some reason, finding and replacing all instances of that generic type might be a real pain, compared to changing it in the derived class.

Finally, there is another reason why someone might wish to create their own types derived from generics. Consider the following classes:

public class MyClass
{
    public ICollection<MyItems> MyItems { get; private set; }
    // plumbing code here
}

public class MyOtherClass
{
    public ICollection<MyItems> MyItemCache { get; private set; }
    // plumbing code here
}

public class MyConsumerClass
{
    public MyConsumerClass(ICollection<MyItems> myItems)
    {
        // use the collection
    }
}

Now how do we know which ICollection<MyItems> to pass into the constructor for MyConsumerClass? If we create derived classes class MyItems : ICollection<MyItems> and class MyItemCache : ICollection<MyItems>, MyConsumerClass can then be extremely specific about which of the two collections it actually wants. This then works very nicely with an IoC container, which can resolve the two collections very easily. It also allows the programmer to be more accurate - if it makes sense for MyConsumerClass to work with any ICollection, they can take the generic constructor. If, however, it never makes business sense to use one of the two derived types, the developer can restrict the constructor parameter to the specific type.

In summary, it is often worthwhile to derive types from generic types - it allows for more explicit code where appropriate and helps readability and maintainability.

3
  • 12
    That's an absolutely ridiculous Microsoft recommendation. Dec 20, 2014 at 15:39
  • 9
    I don't think it's that ridiculous for public APIs. Nested generics are difficult to understand at a glance and a more meaningful type name can often aid readability.
    – Stephen
    Jan 4, 2015 at 22:52
  • 5
    I don't think it's ridiculous, and it's certainly okay for private APIs... but for public APIs I don't think it's a good idea. Even Microsoft recommend accepting and returning the basest of types when you're writing APIs meant for public consumption. Feb 20, 2015 at 17:49
37

In principle there's no difference between inheriting from generic types and inheriting from anything else.

However, why on earth would you derive from any class and not add anything further?

9
  • 18
    I agree. Without contributing anything, I'd read "StringList" and think to myself, "What does it really do?"
    – Neil
    Dec 17, 2014 at 11:05
  • 1
    I agree as well, in may language to inherit you use the key word "extends", this is a good reminder that if you're going to inherit a class then you should be extending it with further functionality. Dec 17, 2014 at 16:01
  • 15
    -1, for not understanding that this is just one way for creating an abstraction by choosing a different name - creating an abstractions can be a very good reason for not adding anything to this class.
    – Doc Brown
    Dec 17, 2014 at 21:27
  • 1
    Consider my answer, I go into some of the reasons someone might decide to do this.
    – NPSF3000
    Dec 17, 2014 at 22:46
  • 1
    "why on earth would you derive from any class and not add anything further?" I've done it for user controls. You can create a generic user control but you can't use it in a windows form designer, so you have to inherit from it, specifying the type, and use that one.
    – George T
    Dec 18, 2014 at 10:34
13

I don't know C# very well, but I believe this is the only way to implement a typedef, which C++ programmers commonly use for a few reasons:

  • It keeps your code from looking like a sea of angle brackets.
  • It lets you swap out different underlying containers if your needs change later, and makes it clear you reserve the right to do so.
  • It lets you give a type a name that is more relevant in context.

They basically threw away the last advantage by choosing boring, generic names, but the other two reasons still stand.

9
  • 7
    Eh... They're not quite the same. In C++ you have a literal alias. StringList is a List<string> - they can used interchangeably. This is important when working with other APIs (or when exposing your API), since everyone else in the world uses List<string>.
    – Telastyn
    Dec 17, 2014 at 14:06
  • 2
    I realize they're not exactly the same thing. Just as close as is available. There are certainly drawbacks to the weaker version. Dec 17, 2014 at 14:42
  • 24
    No, using StringList = List<string>; is the closest C# equivalent of a typedef. Subclassing like this will prevent use of any constructors with arguments. Dec 17, 2014 at 17:36
  • 4
    @PeteKirkham: defining a StringList by using restricts it to the source code file where the using is placed. From this point of view, inheritance is closer to typedef. And creating a subclass does not prevent one from adding all the constructors needed (though it may be tedious).
    – Doc Brown
    Dec 17, 2014 at 21:19
  • 2
    @Random832: let me express this differently: in C++, a typedef can be used to assign the name in one place, to make it a "single source of truth". In C#, using cannot be used for this purpose, but inheritance can. This shows why I disagree to Pete Kirkham's upvoted comment - I don't consider using as a real alternative to C++'s typedef.
    – Doc Brown
    Dec 18, 2014 at 8:26
9

I can think of at least one practical reason.

Declaring non-generic types that inherit from generic types (even without adding anything), allows those types to be referenced from places where they would otherwise be unavailable, or available in a more convoluted way than they should be.

One such case is when adding settings through the Settings editor in Visual Studio, where only non-generic types seem to be available. If you go there, you'll notice that all System.Collections classes are available, but no collections from System.Collections.Generic appear as applicable.

Another case is when instantiating types through XAML in WPF. There is no support for passing type arguments in the XML syntax when using a pre-2009 schema. This is made worse by the fact that the 2006 schema seems to be the default for new WPF projects.

1
  • 1
    In WCF web service interfaces you can use this technique to provide better looking collection type names (eg. PersonCollection instead of ArrayOfPerson or whatever)
    – Rico Suter
    Dec 18, 2014 at 9:51
7

I think you need to look into some history.

  • C# has been used for a lot longer than it has had generic types for.
  • I expect that the given software had its own StringList at some point in history.
  • This may well have been implemented by wrapping an ArrayList.
  • Then someone a to refactoring to move the code base forward.
  • But did not wish to touch 1001 different files, so finish the job.

Otherwise Theodoros Chatzigiannakis had the best answers, e.g. there are still lots of tools that do not understand generic types. Or maybe even a mix of his answer and history.

2
4

In some cases it is impossible to use generic types, for example you can't reference a generic type in XAML. In these cases you can create a non-generic type which inherits from the generic type you originally wanted to use.

If possible, I would avoid it.

Unlike a typedef, you create a new type. If you use the StringList as an input parameter to a method, your callers can't pass a List<string>.

Also, you would need to explicitly copy all constructors you whish to use, in the StringList example you couldn't say new StringList(new[]{"a", "b", "c"}), even though List<T> defines such a constructor.

Edit:

The accepted answer focuses on the pro's when using the derived types inside your domain model. I aggree that they can potentially be useful in this case, still, I personally would avoid them even there.

But you should (in my opinion) never use them in a public API because you either make your caller's life harder or waste performance (I also don't really like implicit conversions in general).

5
  • 3
    You say, "you can't reference a generic type in XAML", but I'm not sure what you're referring to. See Generics in XAML
    – p.s.w.g
    Dec 17, 2014 at 16:28
  • 1
    It was meant as an example. Yes, it is possible with the 2009 XAML Schema, but AFAIK not with the 2006 Schema, which is still the VS default. Dec 18, 2014 at 0:03
  • 1
    Your third paragraph is just half true, you can actually allow this with implicit converters ;)
    – Knerd
    Dec 22, 2014 at 18:55
  • 1
    @Knerd, true, but then you 'implicitly' create a new object. Unless you do a lot more work, this will also create a copy of the data. Probably doesn't matter with small lists, but have fun, if someone passes a list with a few thousand elements =) Dec 22, 2014 at 19:05
  • @LukasRieger you are right :P I just wanted to point that out :P
    – Knerd
    Dec 23, 2014 at 10:26
2

For what it's worth, here's an example of how inheriting from a generic class is used in Microsoft's Roslyn compiler, and without even changing the name of the class. (I was so flummoxed by this that I ended up here in my search to see if this was really possible.)

In project CodeAnalysis you can find this definition:

/// <summary>
/// Common base class for C# and VB PE module builder.
/// </summary>
internal abstract class PEModuleBuilder<TCompilation, TSourceModuleSymbol, TAssemblySymbol, TTypeSymbol, TNamedTypeSymbol, TMethodSymbol, TSyntaxNode, TEmbeddedTypesManager, TModuleCompilationState> : CommonPEModuleBuilder, ITokenDeferral
    where TCompilation : Compilation
    where TSourceModuleSymbol : class, IModuleSymbol
    where TAssemblySymbol : class, IAssemblySymbol
    where TTypeSymbol : class
    where TNamedTypeSymbol : class, TTypeSymbol, Cci.INamespaceTypeDefinition
    where TMethodSymbol : class, Cci.IMethodDefinition
    where TSyntaxNode : SyntaxNode
    where TEmbeddedTypesManager : CommonEmbeddedTypesManager
    where TModuleCompilationState : ModuleCompilationState<TNamedTypeSymbol, TMethodSymbol>
{
  ...
}

Then in project CSharpCodeanalysis there's this definition:

internal abstract class PEModuleBuilder : PEModuleBuilder<CSharpCompilation, SourceModuleSymbol, AssemblySymbol, TypeSymbol, NamedTypeSymbol, MethodSymbol, SyntaxNode, NoPia.EmbeddedTypesManager, ModuleCompilationState>
{
  ...
}

This non-generic PEModuleBuilder class is used extensively in the CSharpCodeanalysis project, and several classes in that project inherit from it, directly or indirectly.

And then in project BasicCodeanalysis there's this definition:

Partial Friend MustInherit Class PEModuleBuilder
    Inherits PEModuleBuilder(Of VisualBasicCompilation, SourceModuleSymbol, AssemblySymbol, TypeSymbol, NamedTypeSymbol, MethodSymbol, SyntaxNode, NoPia.EmbeddedTypesManager, ModuleCompilationState)

Since we can (hopefully) assume that Roslyn was written by people with extensive knowledge of C# and how it should be used I'm thinking that this is a recommendation of the technique.

1
  • Yes. And also they can be used to refine the where clause directly when declared.
    – Sentinel
    May 28, 2018 at 20:55
1

There are three possible reasons why someone would try to do that off the top of my head:

1) To simplify declarations:

Sure StringList and ListString are about the same length but imagine instead you're working with a UserCollection that's actually Dictionary<Tuple<string,Type>, IUserData<Dictionary,MySerializer>> or some other large generic example.

2) To help AOT:

Sometimes you need to use Ahead Of Time compilation not Just In Time Compilation - e.g. developing for iOS. Sometimes the AOT compiler has difficulty figuring out all the generic types ahead of time, and this could be an attempt to provide it with a hint.

3) To add functionality or remove dependency:

Maybe they have specific functionality that they want to implement for StringList (could be hidden in an extension method, not added yet, or historical) that they either can't add to List<string> without inheriting from it, or that they don't want to pollute List<string> with.

Alternatively they may wish to change the implementation of StringList down the track, so have just used the class as a marker for now (usually you'd make/use interfaces for this e.g. IList).


I'd also note that you've found this code in Irony, which is a language parser - a fairly unusual kind of application. There may be some other specific reason that this was used.

These examples demonstrate that there can be legitimate reasons to write such a declaration - even if it's not too common. As with anything consider several options and pick which is best for you at the time.


If you have a look at the source code it appears option 3 is the reason - they use this technique to help add functionality/specialize collections:

public class StringList : List<string> {
    public StringList() { }
    public StringList(params string[] args) {
      AddRange(args);
    }
    public override string ToString() {
      return ToString(" ");
    }
    public string ToString(string separator) {
      return Strings.JoinStrings(separator, this);
    }
    //Used in sorting suffixes and prefixes; longer strings must come first in sort order
    public static int LongerFirst(string x, string y) {
      try {//in case any of them is null
        if (x.Length > y.Length) return -1;
      } catch { }
      if (x == y) return 0;
      return 1; 
    }
2
  • 1
    Sometimes the quest to simplify declarations (or simplify whatever) can result in increased complexity, rather than reduced complexity. Everybody who knows the language moderately well knows what a List<T> is, but they have to inspect StringList in context to actually understand what it is. In reality, it's just List<string>, going by a different name. There really doesn't seem to be any semantic advantage to doing this in such a simple case. You're really just replacing a well-known type with the same type going by a different name. Dec 18, 2014 at 6:15
  • @Craig I agree, as noted by my explanation of usecase (longer declarations) and other possible reasons.
    – NPSF3000
    Dec 18, 2014 at 6:32
-1

I'd say it is not good practice.

List<string> communicates "a generic list of strings". Clearly List<string> extension methods apply. Less complexity is required to understand; only List is needed.

StringList communicates "a need for a separate class". Maybe List<string> extension methods apply (check the actual type implementation for this). More complexity is needed to understand the code; List and the derived class implementation knowledge is required.

7
  • 4
    Extension methods apply anyway. Dec 17, 2014 at 15:29
  • 2
    Off course. But you don't know that untill you inspect the StringList and see it derives from List<string>.
    – Ruudjah
    Dec 18, 2014 at 15:26
  • @TheodorosChatzigiannakis I wonder if Ruudjah could elaborate on what he means by "extension methods don't apply." Extension methods are a possibility with any class. Sure, the .NET framework library ships with a lot of extension methods gratuitously called LINQ that apply to the generic collection classes, but that doesn't mean extension methods don't apply to any other classes? Maybe Ruudjah is making a point that isn't obvious at first glance? ;-) Dec 19, 2014 at 5:38
  • "extension methods don't apply." Where do you read this? I'm saying "extension methods may apply.
    – Ruudjah
    Dec 19, 2014 at 8:40
  • 1
    The type implementation won't tell you whether or not extension methods apply, though, right? Just the fact that it's a class means extension methods do apply. Any consumer of your type can create extension methods completely independently of your code. I guess that's what I'm getting at or asking. Are you just talking about LINQ? LINQ is implemented using extension methods. But the absence of LINQ-specific extension methods for a particular type doesn't mean that extension methods for that type don't exist, or won't. They could be created at any time by anyone. Dec 21, 2014 at 7:11

Not the answer you're looking for? Browse other questions tagged or ask your own question.