C# Record实现带可空成员的接口时的访问器冲突问题及解决
问题背景
我定义了如下接口:
public interface IEmailModel { EmailAddressDto? Receiver { get; set; } }
尝试用Record实现该接口:
public record EmailModelSendConfirmationToUserAndSuperadmin(EmailAddressDto? Receiver = null) : IEmailModel;
此实现无报错。但当实现另一个Record时:
public record EmailModelSendConfirmationEmailCodeToUser(string link, EmailAddressDto? Receiver = null) : IEmailModel;
出现错误:
'EmailModelSendConfirmationEmailCodeToUser' does not implement interface member 'IEmailModel.Receiver.set'.
'EmailModelSendConfirmationEmailCodeToUser.Receiver.init' cannot implement 'IEmailModel.Receiver.set'.
我将接口修改为:
public interface IEmailModel { EmailAddressDto? Receiver { get; init; } }
此时第二个Record无报错,但第一个Record出现错误:
'EmailModelSendConfirmationToUserAndSuperadmin' does not implement interface member 'IEmailModel.Receiver.init'.
'EmailModelSendConfirmationToUserAndSuperadmin.Receiver.set' cannot implement 'IEmailModel.Receiver.init'.
请问为何会出现这种情况,如何用Record正确实现该接口?
原因分析
这是因为C#编译器会根据record的主构造函数参数数量和默认值自动推断属性的setter类型:
- 当record的主构造函数只有一个带默认值的参数(如第一个
EmailModelSendConfirmationToUserAndSuperadmin),编译器会自动生成带set的属性,允许对象初始化后修改属性。 - 当record的主构造函数有多个参数(即使其中一个带默认值,如第二个
EmailModelSendConfirmationEmailCodeToUser的link和Receiver),编译器会生成带init的属性,这类record通常被设计为不可变对象,仅允许在初始化阶段设置属性。
接口的get; set;要求实现类提供可读写属性,get; init;要求实现类提供仅初始化阶段可写的属性,两者无法互相兼容,因此会出现互相报错的情况。
正确实现方式
有两种可靠的解决方法:
方法1:显式指定属性的setter类型
在record中显式定义接口的属性,明确指定setter类型,确保和接口定义一致:
如果接口使用get; set;:
public interface IEmailModel { EmailAddressDto? Receiver { get; set; } } // 第一个record可保持自动生成(编译器已生成带set的属性) public record EmailModelSendConfirmationToUserAndSuperadmin(EmailAddressDto? Receiver = null) : IEmailModel; // 第二个record显式实现带set的属性 public record EmailModelSendConfirmationEmailCodeToUser(string link) : IEmailModel { public EmailAddressDto? Receiver { get; set; } = null; }
如果接口使用get; init;:
public interface IEmailModel { EmailAddressDto? Receiver { get; init; } } // 第一个record显式实现带init的属性 public record EmailModelSendConfirmationToUserAndSuperadmin : IEmailModel { public EmailAddressDto? Receiver { get; init; } = null; } // 第二个record保持自动生成即可(编译器已生成带init的属性) public record EmailModelSendConfirmationEmailCodeToUser(string link, EmailAddressDto? Receiver = null) : IEmailModel;
方法2:统一显式实现接口
不管接口定义的是set还是init,所有record都显式定义Receiver属性,避免编译器自动推断带来的差异:
public interface IEmailModel { EmailAddressDto? Receiver { get; set; } // 或改为get; init; } public record EmailModelSendConfirmationToUserAndSuperadmin : IEmailModel { public EmailAddressDto? Receiver { get; set; } = null; // 对应接口的set,或改成init对应接口的init } public record EmailModelSendConfirmationEmailCodeToUser(string link) : IEmailModel { public EmailAddressDto? Receiver { get; set; } = null; // 同上 }
内容的提问来源于stack exchange,提问作者Oleg Sh

