【问题标题】:What is the best approach or alternative to constant references?恒定引用的最佳方法或替代方法是什么?
【发布时间】:2019-04-27 01:56:15
【问题描述】:

就本问题而言,“常量引用”是对无法调用修改对象或修改其属性的方法的对象的引用。

我想要这样的东西:

Const<User> user = provider.GetUser(); // Gets a constant reference to an "User" object
var name = user.GetName(); // Ok. Doesn't modify the object
user.SetName("New value"); // <- Error. Shouldn't be able to modify the object

理想情况下,我会用自定义属性(例如[Constant])标记类的每个不修改实例的方法,并且只能从常量引用中调用这些方法。如果可能,在编译期间调用其他方法会导致错误。

这个想法是我可以返回一个只读引用并确保它不会被客户端修改。

【问题讨论】:

  • 这被称为“const- 正确性”,这是 C++ 和 Swift 的语言特性,但不是 C#,不幸的是 - 但是你可以通过使用自定义属性来做一些事情,因为这样你可以通过 Roslyn 扩展执行它 - 但这是一个兔子洞。
  • 其实是不可变的。
  • 如果您将所有属性设为虚拟,您可以在从同一个对象继承时发出代理。在这种情况下,您实际上可以执行User u = prov.GetUser(); // gets proxy
  • @T.S.被覆盖的virtual 属性无法删除属性设置器,这意味着您不会收到编译器警告,即使被覆盖的设置器抛出NotSupportedException
  • @Dai 没错。无法删除。但是 OP 说:“在编译期间调用其他方法会导致错误,如果可能。” 所以,我的建议是有效的。我喜欢它,因为它不是“太复杂”,从它继承的类发出代理

标签: c# const-correctness compile-time-constant


【解决方案1】:

您所指的技术称为"const-correctness",这是 C++ 和 Swift 的语言功能,但不是 C#,不幸的是 - 但是您可以通过使用自定义属性来完成某些事情,因为这样您可以通过Roslyn 扩展 - 但这是一个兔子洞。

另外,使用接口有一个更简单的解决方案:因为 C#(我也认为 CLR)不支持 const 正确性(我们拥有的最接近的是 readonly 字段修饰符).NET 基类库设计人员为常见的可变类型添加了“只读接口”,以允许对象(无论是可变的还是不可变的)通过只公开不可变操作的接口公开其功能。一些示例包括 IReadOnlyList&lt;T&gt;IReadOnlyCollection&lt;T&gt;IReadOnlyDictionary&lt;T&gt; - 虽然这些都是可枚举类型,但该技术也适用于单个对象。

这种设计的优点是可以使用任何支持接口但不支持 const 正确性的语言。

  1. 对于您的项目中需要公开数据而不会有被更改风险的每种类型(classstruct 等)或任何不可变操作然后创建不可变接口。
  2. 修改您的消费代码以使用这些接口而不是具体类型。

像这样:

假设我们有一个可变类 User 和一个消费服务:

public class User
{
    public String UserName     { get; set; }

    public Byte[] PasswordHash { get; set; }
    public Byte[] PasswordSalt { get; set; }

    public Boolean ValidatePassword(String inputPassword)
    {
        Hash[] inputHash = Crypto.GetHash( inputPassword, this.PasswordSalt );
        return Crypto.CompareHashes( this.PasswordHash, inputHash );
    }

    public void ResetSalt()
    {
        this.PasswordSalt = Crypto.GetRandomBytes( 16 );
    }
}

public static void DoReadOnlyStuffWithUser( User user )
{
    ...
}

public static void WriteStuffToUser( User user )
{
    ...
}

然后制作一个不可变的接口:

public interface IReadOnlyUser
{
    // Note that the interfaces' properties lack setters.
    String              UserName     { get; }
    IReadOnlyList<Byte> PasswordHash { get; }
    IReadOnlyList<Byte> PasswordSalt { get; }

    // ValidatePassword does not mutate state so it's exposed
    Boolean ValidatePassword(String inputPassword);

    // But ResetSalt is not exposed because it mutates instance state
}

然后修改你的User 类和消费者:

public class User : IReadOnlyUser
{
    // (same as before, except need to expose IReadOnlyList<Byte> versions of array properties:
    IReadOnlyList<Byte> IReadOnlyUser.PasswordHash => this.PasswordHash;
    IReadOnlyList<Byte> IReadOnlyUser.PasswordSalt => this.PasswordSalt;
}

public static void DoReadOnlyStuffWithUser( IReadOnlyUser user )
{
    ...
}

// This method still uses `User` instead of `IReadOnlyUser` because it mutates the instance.
public static void WriteStuffToUser( User user )
{
    ...
}

【讨论】:

  • 旁注:单独的接口不会保护不友好的调用者,因为它们可以使用反射/cast/dynamic 来调用实现类的SetXxxx 方法。如果是这种情况,您需要隐藏实际类的接口和代理实现。
  • @AlexeiLevenkov 这与在 C++ 中使用 const_cast 没有什么不同。我认为无论如何开发对抗性图书馆消费者的对策是不值得的,因为这是为了消除潜在的错误,而不是安全。
  • 确实在友好的客户端界面的情况下就足够了。目前尚不清楚 OP 正在寻找多少保护“确保它不会被客户端修改”可以通过多种不同方式读取。
  • @Alexei Levenkov 我的意图正如戴所说的那样,与防止潜在错误有关,而不是安全性。很抱歉没有完全清楚。不过观察力很好。
【解决方案2】:

所以,这是我最初的两个想法,但并没有完全解决问题。

使用动态对象:

我的第一个想法是创建一个Dynamic Object,如果被调用的方法没有标记[Constant] 自定义属性,它将拦截所有成员调用并抛出错误。这种方法是有问题的,因为 a) 在处理动态对象时,我们没有编译器的支持来检查代码中的错误(即方法名称拼写错误),这可能会导致很多运行时错误; b) 我打算经常使用它,每次调用方法时按名称搜索方法名称可能会对性能产生相当大的影响。

使用 RealProxy:

我的第二个想法是使用RealProxy 包装真实对象并验证被调用的方法,但这仅适用于从MarshalByRefObject 继承的对象。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-11
    • 1970-01-01
    • 2022-11-13
    • 2013-04-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多