【问题标题】:Immutable views of mutable types可变类型的不可变视图
【发布时间】:2010-11-12 19:36:01
【问题描述】:

我有一个项目,我需要在执行流程之前构建大量配置数据。在配置阶段,将数据设置为可变非常方便。但是,一旦配置完成,我想将该数据的不可变视图传递给功能流程,因为该流程的许多计算将依赖于配置不变性(例如,基于预先计算事物的能力关于初始配置。)我想出了一个可能的解决方案,使用接口来公开只读视图,但我想知道是否有人遇到过这种方法的问题,或者是否有其他关于如何解决的建议解决这个问题。

我目前使用的模式的一个示例:

public interface IConfiguration
{
    string Version { get; }

    string VersionTag { get; }

    IEnumerable<IDeviceDescriptor> Devices { get; }

    IEnumerable<ICommandDescriptor> Commands { get; }
}

[DataContract]
public sealed class Configuration : IConfiguration
{
    [DataMember]
    public string Version { get; set; }

    [DataMember]
    public string VersionTag { get; set; }

    [DataMember]
    public List<DeviceDescriptor> Devices { get; private set; }

    [DataMember]
    public List<CommandDescriptor> Commands { get; private set; }

    IEnumerable<IDeviceDescriptor> IConfiguration.Devices
    {
        get { return Devices.Cast<IDeviceDescriptor>(); }
    }

    IEnumerable<ICommandDescriptor> IConfiguration.Commands
    {
        get { return Commands.Cast<ICommandDescriptor>(); }
    }

    public Configuration()
    {
        Devices = new List<DeviceDescriptor>();
        Commands = new List<CommandDescriptor>();
    }
}

编辑

根据 Lippert 先生和 cdhowie 的意见,我整理了以下内容(删除了一些属性以简化):

[DataContract]
public sealed class Configuration
{
    private const string InstanceFrozen = "Instance is frozen";

    private Data _data = new Data();
    private bool _frozen;

    [DataMember]
    public string Version
    {
        get { return _data.Version; }
        set
        {
            if (_frozen) throw new InvalidOperationException(InstanceFrozen);
            _data.Version = value;
        }
    }

    [DataMember]
    public IList<DeviceDescriptor> Devices
    {
        get { return _data.Devices; }
        private set { _data.Devices.AddRange(value); }
    }

    public IConfiguration Freeze()
    {
        if (!_frozen)
        {
            _frozen = true;
            _data.Devices.Freeze();
            foreach (var device in _data.Devices)
                device.Freeze();
        }
        return _data;
    }

    [OnDeserializing]
    private void OnDeserializing(StreamingContext context)
    {
        _data = new Data();
    }

    private sealed class Data : IConfiguration
    {
        private readonly FreezableList<DeviceDescriptor> _devices = new FreezableList<DeviceDescriptor>();

        public string Version { get; set; }

        public FreezableList<DeviceDescriptor> Devices
        {
            get { return _devices; }
        }

        IEnumerable<IDeviceDescriptor> IConfiguration.Devices
        {
            get { return _devices.Select(d => d.Freeze()); }
        }
    }
}

FreezableList&lt;T&gt; 如您所料,是IList&lt;T&gt; 的可冻结实现。这获得了绝缘优势,但代价是一些额外的复杂性。

【问题讨论】:

  • 你真的需要公开Configuration吗?
  • @Vlad,支持单独的配置编辑器项目是公开的。

标签: c# .net interface immutability mutability


【解决方案1】:

如果“客户端”(接口的消费者)和“服务器”(类的提供者)达成以下共识,那么您描述的方法非常有效:

  • 客户端会很有礼貌,不会试图利用服务器的实现细节
  • 在客户端引用对象后,服务器会礼貌且不会改变对象。

如果您在编写客户端的人员和编写服务器的人员之间没有良好的工作关系,那么事情很快就会变成梨形。粗鲁的客户当然可以通过转换为公共配置类型来“抛弃”不变性。粗鲁的服务器可以分发一个不可变的视图,然后在客户端最不期望的时候改变对象。

一个很好的方法是防止客户端看到可变类型:

public interface IReadOnly { ... }
public abstract class Frobber : IReadOnly
{
    private Frobber() {}
    public class sealed FrobBuilder
    {
        private bool valid = true;
        private RealFrobber real = new RealFrobber();
        public void Mutate(...) { if (!valid) throw ... }
        public IReadOnly Complete { valid = false; return real; }
    }
    private sealed class RealFrobber : Frobber { ... }
}

现在如果你想创建和改变一个 Frobber,你可以创建一个 Frobber.FrobBuilder。完成突变后,调用 Complete 并获得一个只读界面。 (然后构建器变得无效。)由于所有可变性实现细节都隐藏在私有嵌套类中,因此您不能将 IReadOnly 接口“丢弃”到 RealFrobber,而只能“丢弃”到没有公共方法的 Frobber!

恶意客户端也不能创建自己的 Frobber,因为 Frobber 是抽象的并且有一个私有构造函数。制作 Frobber 的唯一方法是通过构建器。

【讨论】:

  • 我喜欢这个解决方案,虽然它确实让突变有点尴尬。目前,我拥有应用程序的客户端和服务器部分,但我可以看到当维护工程师认为“抛弃”不变性或忘记服务器应该方便时可能出现的问题t 交接后修改配置。我对这种方法的主要担忧是,与我共事过的一些初级开发人员很难理解这种结构。
  • 我认为您的意思是让 RealFrobber 实现 IReadOnly,而不是 Frobber?否则,Frobber 必须具有公共方法或至少具有显式接口实现。我不得不将其标记为解决方案,只是为了它的偏执狂的完整性; “虚拟”外部类意味着即使是同一程序集中的其他类也不能破坏不变性契约。
  • 附带说明,难道你不能完全摆脱 Frobber,只使用 RealFrobber 作为 FrobBuilder 的嵌套私有类吗?
  • @Dan:是的,有很多方法可以做到这一点。我建议的方式只是一种可能性。这种模式的一个常见用途是拥有一堆不同的特定类型的 Frobber 构建器可以构建,它们都是实现细节,并且都实现了基类型的所有接口,但您不必这样做。
  • @Dan Bryant:另一种设计是有一个抽象类型 FrobBase,它为所有感兴趣的东西提供只读属性,有一个密封的子类 ImmutableFrob 和一个可能未密封的类 MutableFrob。 ImmutableFrob 应该有一个从 FrobBase 复制其数据的构造函数。想要已知不可变的代码可以采用 ImmutableFrob 类型的对象。想要它可以变异的代码将使用 MutableFrob。不会改变对象本身的代码,但不会关心其他东西可能会使用 FrobBase。
【解决方案2】:

这会起作用,但“恶意”方法可能会尝试将IConfiguration 转换为Configuration,从而绕过您的接口强加限制。如果您对此不担心,那么您的方法就可以正常工作。

我通常会这样做:

public class Foo {
    private bool frozen = false;

    private string something;

    public string Something {
        get { return something; }
        set {
            if (frozen)
                throw new InvalidOperationException("Object is frozen.");

            // validate value

            something = value;
        }
    }

    public void Freeze() {
        frozen = true;
    }
}

或者,您可以将可变类深度克隆为不可变类。

【讨论】:

  • 您对演员阵容的脆弱性是正确的,但是这两个建议都破坏了链接。为了通过不可变视图可见的可变视图进行更改,有必要添加只读代理/包装器对象。使用包含而不是继承来重用可以防止强制转换。
  • OP 没有以任何方式表明客户端可以在服务器处理它时更改不可变视图。
  • 好吧,一个真的恶意客户端甚至可以通过反射恢复frozen的值。
  • @Vlad:cdhowie 是对的。以具有完全信任的恶意代码为前提的攻击不是有趣的攻击。完全信任的代码不必使用反射来改变“冻结”的值;它可以使用不安全的代码来获取指向内存的指针,然后直接进入并丢弃它想要更改的位。完全受信任的代码可以启动一个附加到进程的调试器,暂停所有线程,完全重写内部内存结构,然后恢复线程。完全受信任的代码可以做您机器所有者可以做的任何事情。
  • @Dan:没有什么说你也不能使用界面。然后客户端将看不到设置的访问器,但围绕接口进行强制转换不会让它们获得更多访问权限;他们只会受到例外的欢迎。
【解决方案3】:

为什么不能提供对象的单独不可变视图?

public class ImmutableConfiguration {
    private Configuration _config;
    public ImmutableConfiguration(Configuration config) { _config = config; }
    public string Version { get { return _config.Version; } }
}

或者,如果您不喜欢额外的输入,请将集合成员设置为内部而不是公共的 - 在程序集中可访问但不能由程序集的客户端访问?

【讨论】:

  • 不可变包装器是一种很好的方法,并且有很好的使用示例(尤其是 ReadOnlyCollection)。这也比 Eric 的方法更容易理解,但它只能防止客户端执行强制转换(“粗鲁”的服务器在传递视图后仍然可以改变配置。)
【解决方案4】:

我经常使用基于 COM 的大型框架(ESRI 的 ArcGIS Engine),它在某些情况下处理修改非常相似:有“默认”IFoo 接口用于只读访问,IFooEdit接口(如果适用)进行修改。

这个框架相当有名,我不知道有任何关于它背后的特定设计决策的广泛抱怨。

最后,我认为在决定哪个“透视图”成为默认透视图时绝对值得多考虑一下:只读透视图或完全访问透视图。我个人会将只读视图设为默认值。

【讨论】:

    【解决方案5】:

    怎么样:

    struct Readonly<T>
    {
        private T _value;
        private bool _hasValue;
    
        public T Value
        {
            get
            {
                if (!_hasValue)
                    throw new InvalidOperationException();
                return _value;
            }
            set
            {
                if (_hasValue)
                    throw new InvalidOperationException();
                _value = value;
            }
        }
    }
    
    
    [DataContract]
    public sealed class Configuration
    {
        private Readonly<string> _version;
    
        [DataMember]
        public string Version
        {
            get { return _version.Value; }
            set { _version.Value = value; }
        }
    }
    

    我称它为 Readonly,但我不确定这是不是它的最佳名称。

    【讨论】:

    • 这可以在运行时通过抛出异常来强制开发人员的不可变性,但是它使得在编译时很难检测到对象是真正不可变的。开发人员(以及最终用户,如果提供了错误使用的代码)直到运行时(如果有的话)才会发现假设可变性开发的代码永远不会按预期工作。另外,我将其称为SetOnce 而不是ReadOnly,因为它更准确地描述了它的机制。
    • 不存在不可变结构类型;在您的示例中,任何可以访问_version.Value 并想要任意重写它的代码都可以说_version = new ReadOnly&lt;string&gt;(); _version.Value = whatever;。即使向ReadOnly&lt;T&gt; 结构添加参数化构造函数并且没有提供其他变异方法,ReadOnly&lt;T&gt; 类型的字段也不会提供比T 类型的字段更多的保护。
    猜你喜欢
    • 2011-12-24
    • 1970-01-01
    • 1970-01-01
    • 2012-02-28
    相关资源
    最近更新 更多