简体   繁体   中英

Pass In Type as Constructor Parameter or Generic

I created a logger with the following interface

public interface ILogger
{
    void Log(LogLevel logLevel, string message, [CallerMemberName] string callingMemberName = "");
    Exception Log(Exception ex, [CallerMemberName] string callingMemberName = "");
}

One of the thing I want printed when Log() is called is that the method name along with its Type should be printed. The method name is easy to get with [CallerMemberName] attribute. To get the type I either need to use the StackTrace (which is slow and unpredictable) or pass it in.

I decided I wanted to pass it in, but came up with ways to do this.

1) Pass it in to the constructor

public abstract class AbstractLogger : ILogger
{
    protected LogLevel minLogLevel;
    protected Type callingMemberType;

    protected AbstractLogger(Type callingMemberType, LogLevel minLogLevel)
    {
        this.callingMemberType = callingMemberType;
        this.minLogLevel = minLogLevel;
    }

    //abstract methods omitted
}

2) Pass it in as a generic

public abstract class AbstractLogger<T> : ILogger
{
    protected LogLevel minLogLevel;
    protected Type callingMemberType;

    protected AbstractLogger(LogLevel minLogLevel)
    {
        this.callingMemberType = typeof(T);
        this.minLogLevel = minLogLevel;
    }

    //abstract methods omitted
}

Both require each class to have its own ILogger instance, but I'm ok with that.

This is what a call would look like for each of them:

//pass in to constructor
public ILogger MyLogger = new ConcreteLogger(typeof(MyClass, LogLevel.DEBUG);

//pass as a generic
public ILogger MyLogger = new ConcreteLogger<MyClass>(LogLevel.DEBUG);

The question is, is there any reason to prefer one method over the other?

Both ways are working fine in your case.

But there might be some consideration had the case been different. For instance, if the T is derived from some other class/implements an interface (constrained generic) such that it needs to call a method somewhere in the code, then using generic is more beneficial as you can call the method directly (ie the non-generic will require Reflection):

  public class Foo {
    public void Execute() { }
  }

  public class Bar<T> where T : Foo { //constrained generic
    public T val;
    public Bar(T input){
      val = input;
    }
    public void SomeFunction() {
      val.Execute(); //generic is better since it can call this "Execute" method without Reflection
    }
  }

But in your case, there is no need for that. Until there is further code, both cases should be fine.

I personally will prefer to code as per necessary. In this case, being no need for generic, I would use Type .

Two main differences

Run-time versus compile-time

Using generics, you must know the type at compile time. This may be a little tricky if your logging code is embedded in a helper library, since the helper methods or classes will also have to expose a generic parameter in order to pass it along. Meanwhile, using the constructor argument approach you can determine the type at run-time and pass it around as a Type or even as a string with no issue.

Static trouble

Each "version" of a generic class is its own .NET type. That means each one has its own static constructor and variables. This can make a huge difference.

Imagine your logger maintains a single handle-per-process for file output:

class Logger
{
    static private FileStream _fileStream;
    static private TextWriter _writer;

    static void Logger()
    {
        var config = ReadConfigurationFile();
        _fileStream = new FileStream(config.path);
        _writer = new TextWriter(_fileStream);
    }
}

void Main()
{
    var l1 = new Logger("MyType");  //On first invocation, will fire static constructor and reserve the file
    var l2 = new Logger("SomeOtherType"); //Static constructor has already run, and won't run again
}

In this example, there is one FileStream and TextWriter that will be shared by all instances of Logger . Simple, straighforward, and makes sense; after all, there is only one file, so why open more than one handle?

Now look at generics:

class Logger<T> where t : class
{
    static private FileStream _fileStream;
    static private TextWriter _writer;

    static void Logger()
    {
        var config = ReadConfigurationFile();
        _fileStream = new FileStream(config.path);
        _writer = new TextWriter(_fileStream);
    }
}

void Main()
{
    var l1 = new Logger<HomePage>(); //Fires the static constructor for Logger<HomePage> on first invocation
    var l2 = new Logger<HelpPage>(); //Fires a different static constructor, and will try to open the file a second time
}

Under this model, a Logger<HomePage> is technically a different .NET type from a Logger<HelpPage> . Since it is a different type, it'll have a different set of static variables. In this case, each time you instantiate a new type of logger, you are running a new static constructor and trying to open a handle on the same file as all the other loggers. This may end up causing resource contention or other unintended side effects, eg you won't even be able to open the file.

You could get around this by embedding yet another, non-generic class inside, and letting that embedded class contain the static members. Or you could inject an instance-per-process class and hold what you need in member variables. Personally I think that adds unnecessary complexity for something that should be very simple.

Unless there is a compelling reason, I would avoid using generics for this specific case.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM