This is post 1 in a 3-part series building up to the new C# 11 feature called Generic Math. Before tackling that though, let’s check out another new C# 11 feature - the static abstract interface method (aka static virtual members).
What is a static abstract interface method in C#? (this post)
We use interfaces a lot in C#. They’re essentially contracts, so if a class implements a particular interface, then you can be confident that the class includes all the properties and methods defined in that interface. In the following example, EmployeeReport implements everything in IEmployeeReport, and VendorReport implements everything in IVendorReport.
Interfaces can extend one another too, like the two interfaces below are doing with IBaseReport. The classes need to implement everything in that base interface too, like the ones below are doing by defining ReportName and IsSensitive.
Any method that might do something with one of these reports, can operate on the interface instead. The GetReportInfo method below is assured that anything implementing IBaseReport has 2 properties on it. And bonus - we don’t need to define a method for each type of report, just one which can handle any report.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/******************
* A CLASS THAT PROCESSES REPORTS
* *****************/publicclassISwearImAnInterestingClass{publicstringGetVendorReportStatus(IVendorReportrpt){// i.e. Running 'Vendor Summary' for: Acme Increturn$"Running '{rpt.ReportName}' for: {rpt.VendorName}";}publicstringGetReportInfo(IBaseReportrpt){// i.e. Employee Profile is a sensitive report.return$"{rpt.ReportName} {(rpt.IsSensitive ? "is" : "isnot")} a sensitive report.";}}
Aside from acting as a contract and helping us write DRY code, interfaces are useful for unit testing too. If you want to learn more about that, I’ve written before about how interfaces help with mocking dependencies when unit testing.
However, something we can’t do with interfaces is define a static member and have classes implement those. For instance, if you knew you wanted every kind of report to have a ReportName, but you didn’t want to have to instantiate a report just to get to a name that never changes, you couldn’t do something like this…
1
2
3
4
5
6
7
8
9
/******************
* BASE REPORT INTERFACE
* *****************/publicinterfaceIBaseReport{staticstringReportName{get;}boolIsSensitive{get;}}
The other method, the one that references the interface to get the ReportName, will suggest you use an actual instance to get to the static member. Ok sure… except this is an interface so you can’t just instantiate it. Maybe you could add some code in the GetReportInfo method below, to check for every possible report type that implements IBaseReport but then that’d make for some repetitive, hard-to-maintain code.
On top of that, the classes that implement the IBaseReport interface don’t have to include that static member to satisfy the contract with the interface anymore. This (which doesn’t define ReportName) won’t throw a compilation error:
The static abstract concept seems to have been mostly added to support overloaded operators and the other new concept of generic math, but let’s take a look at what else we can do with it, without muddying the waters too much.
I’ve changed the IBaseReport interface (below) to use the new static abstract modifiers. For good measure, I’ve added in a static abstract method to show that those can be static too, and a normal property that’s not static to demonstrate that we can have a mix.
Here’s a few things to look for and keep in mind as you check out the similar, but not-quite-the-same block of code, below:
The classes are required to implement the static abstract members.
The classes are required to implement the GenerateUniqueId method too, but how they choose to implement that can differ wildly per class.
The GetReportInfo method in that last class, and the GetNewReportId method I threw in too, are implemented differently than before. They’re capable of accessing the normal members of an instance, as well as the static members of the class too.
/******************
* BASE REPORT INTERFACE WITH
* STATIC ABSTRACT MEMBERS
* *****************/publicinterfaceIBaseReport{staticabstractstringReportName{get;}staticabstractboolIsSensitive{get;}staticabstractstringGenerateUniqueId();DateTimeRequestedTime{get;set;}}/******************
* EMPLOYEE REPORT w/ INTERFACE
* AND IMPLEMENTING STATIC MEMBERS
* *****************/publicinterfaceIEmployeeReport:IBaseReport{stringName{get;set;}DateTimeHireDate{get;set;}DateTime?TermDate{get;set;}}publicclassEmployeeReport:IEmployeeReport{publicstaticstringReportName=>"Employee Profile";publicstaticboolIsSensitive=>true;publicDateTimeRequestedTime{get;set;}publicstaticstringGenerateUniqueId()=>$"{ReportName.Replace("","")}-{DateTime.Now:yyyy-MM-dd-hh-mm-ss:ffffff}";publicstringName{get;set;}publicDateTimeHireDate{get;set;}publicDateTime?TermDate{get;set;}}/******************
* VENDOR REPORT CLASS w/ INTERFACE
* AND IMPLEMENTING STATIC MEMBERS
* *****************/publicinterfaceIVendorReport:IBaseReport{stringVendorName{get;set;}stringVendorContactName{get;set;}stringVendorContactPhone{get;set;}}publicclassVendorReport:IVendorReport{publicstaticstringReportName=>"Vendor Summary";publicstaticboolIsSensitive=>false;publicDateTimeRequestedTime{get;set;}publicstaticstringGenerateUniqueId()=>$"{Guid.NewGuid()}";publicstringVendorName{get;set;}publicstringVendorContactName{get;set;}publicstringVendorContactPhone{get;set;}}/******************
* A CLASS THAT PROCESSES REPORTS
* *****************/publicclassISwearImAnInterestingClass{publicstringGetVendorReportStatus(IVendorReportrpt){return$"Running '{VendorReport.ReportName}' for: {rpt.VendorName}";}publicstringGetReportInfo<T>(Trpt)whereT:IBaseReport{return$"{T.ReportName} was requested on {rpt.RequestedTime:d}.";}publicstringGetNewReportId<T>()whereT:IBaseReport=>T.GenerateUniqueId();}
Of course, it’s always a good idea to create some tests (and if you’re using WinForms, you might want to brush up onusing MVP to help with testing) to make sure everything looks good, and to prove that the static abstract interface members, as weird as they might look, do actually work.
ISwearImAnInterestingClassic;[SetUp]publicvoidSetup(){ic=new();}[Test]publicvoidISwearImAnInterestingClass_ReturnsExpectedValues_ForEmployeeReport(){varemployeeReport=newEmployeeReport{Name="Bob",HireDate=newDateTime(2010,1,1),RequestedTime=DateTime.Now};Assert.Multiple(()=>{Assert.That(ic.GetReportInfo(employeeReport),Does.StartWith($"{EmployeeReport.ReportName} was requested on "));Assert.That(ic.GetNewReportId<EmployeeReport>(),Does.StartWith("EmployeeProfile-20"));// will fail in 2100 :p});}[Test]publicvoidISwearImAnInterestingClass_ReturnsExpectedValues_ForVendorReport(){varvendorReport=newVendorReport{VendorName="Acme Inc",VendorContactName="Mary",RequestedTime=DateTime.Now};Assert.Multiple(()=>{Assert.That(ic.GetVendorReportStatus(vendorReport),Is.EqualTo($"Running '{VendorReport.ReportName}' for: {vendorReport.VendorName}"));Assert.That(ic.GetReportInfo(vendorReport),Does.StartWith($"{VendorReport.ReportName} was requested on "));Assert.That(Guid.TryParse(ic.GetNewReportId<VendorReport>(),outvar_),Is.True);});}
Unfortunately, this new construct doesn’t seem to play nicely yet with the popular Moq framework that helps you mock out interface calls during testing. I’m most familiar with Moq, but maybe JustMock or TypeMock supports it?
Next up, we’ll check out another C# feature we’ve had for a long time - operator overloading. After that, we’ll take a closer look at what this Generic Math thing is all about.
If you found this content useful, and want to learn more about a variety of C# features, check out this GitHub repo, where you’ll find links to plenty more blog posts and practical examples!