3

I have this class structure (simplified):

public class InducingMedium
{
   public required string File { get; set; }
}

public class InducingVideo : InducingMedium {}
public class InducingAudio : InducingMedium {}

Now, I want to generically instantiate an instance of a specific type:

public abstract class BaseInducingTests<TMedium>
    where TMedium : InducingMedium, new()
{
   protected async Task<IEnumerable<TMedium>> CreateInducingMedia(IEnumerable<string> files)
   {
      return files.Select(file =>
      {
          // Do some processing...

          return new TMedium
          {
              File = file,
          };
      });
   }
}

public class InducingVideosTests : BaseInducingTests<InducingVideo>
{
}

But in the derived class I get an error:

'Namespace.InducingVideo' cannot satisfy the 'new()' constraint 
on parameter 'TMedium' in the generic class 'Namespace.Tests.BaseInducingTests<TMedium>' 
because 'Namespace.InducingVideo' has required members

Is there any way to fix this without introducing reflection?
I was really excited about required members, which work pretty well with nullable types, but now I see this has its own caveats :(

Mr Patience
  • 1,564
  • 16
  • 30

2 Answers2

4

This is explicitly mentioned in the docs:

A type with any required members may not be used as a type argument when the type parameter includes the new() constraint. The compiler can't enforce that all required members are initialized in the generic code.

Either remove the required modifier or change the generic handling. For example provide factory via ctor:

public abstract class BaseInducingTests<TMedium>
    where TMedium : InducingMedium
{
    private readonly Func<string, TMedium> _init;

    public BaseInducingTests(Func<string, TMedium> init)
    {
        _init = init;
    }
    protected async Task<IEnumerable<TMedium>> CreateInducingMedia(IEnumerable<string> files)
    {
        return files.Select(file => _init(file));
    }
}

public class InducingVideosTests : BaseInducingTests<InducingVideo>
{
    public InducingVideosTests() : base(s => new InducingVideo{File = s})
    {
    }
}

If needed you can then create a wrapper to support classes which satisfy new() constraint:

public abstract class BaseNewableInducingTests<TMedium> : BaseInducingTests<TMedium>
    where TMedium : InducingMedium, new()
{
    protected BaseNewableInducingTests() : base(s => new TMedium { File = s })
    {
    }
}
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
2

Another workaround which I've just found is SetsRequiredMembers attribute:

public class InducingVideosTests : BaseInducingTests<TestInducingVideo>
{
}

public class TestInducingVideo : InducingVideo
{
    [SetsRequiredMembers]
    public TestInducingVideo()
    {
    }
}

This kind of defeats the purpose of required members, but so does reflection and for test purposes it's an easier way.

Source: https://code-maze.com/csharp-required-members/

Mr Patience
  • 1,564
  • 16
  • 30
  • Note - this is also mentioned in the docs (referenced in my answer) with a warning: _"The `SetsRequiredMembers` disables the compiler's checks that all required members are initialized when an object is created. Use it with caution."_, one reason being - it does not even check if ctor actually does what is claimed (i.e. sets `File` in this case). – Guru Stron Jan 28 '23 at 10:06