如何在C#中为持久性集合设计api?

如何在C#中为持久性集合设计api?,c#,clojure,api-design,persistent,C#,Clojure,Api Design,Persistent,我正在考虑在C#中创建一个持久的集合(列表或其他),但我想不出一个好的API 我在中使用了“”:持久列表是一个行为类似于具有值语义而不是引用语义的列表,但不会产生复制大值类型的开销。持久集合使用写时复制来共享内部结构。伪代码: l1 = PersistentList() l1.add("foo") l1.add("bar") l2 = l1 l1.add("baz") print(l1) # ==> ["foo", "bar", "baz"] print(l2) # ==> ["f

我正在考虑在C#中创建一个持久的集合(列表或其他),但我想不出一个好的API

我在中使用了“”:持久列表是一个行为类似于具有值语义而不是引用语义的列表,但不会产生复制大值类型的开销。持久集合使用写时复制来共享内部结构。伪代码:

l1 = PersistentList()
l1.add("foo")
l1.add("bar")
l2 = l1
l1.add("baz")

print(l1) # ==> ["foo", "bar", "baz"]
print(l2) # ==> ["foo", "bar"]
# l1 and l2 share a common structure of ["foo", "bar"] to save memory
Clojure使用这样的数据结构,但在Clojure中,所有数据结构都是不可变的。在执行所有写时复制的工作时会有一些开销,因此Clojure提供了一种数据结构形式的变通方法,如果您确定不与其他人共享数据结构,可以使用这种方法。如果您只有一个对数据结构的引用,为什么不直接对它进行变异,而不是遍历所有的写时拷贝开销呢

获得这种效率提高的一种方法是在您的数据结构上保持引用计数(尽管我不认为Clojure是这样工作的)。如果refcount为1,则您持有的是唯一的引用,因此以破坏性方式进行更新。如果refcount更高,则其他人也持有对它的引用,该引用的行为应该类似于值类型,因此请在写入时进行复制,以免干扰其他引用者

在这种数据结构的API中,可能会暴露refcounting,这会严重降低API的可用性,或者无法执行refcounting,如果每个操作都被阻塞,则会导致不必要的写时复制开销,或者API失去其值类型行为,用户必须管理何时手动执行COW

如果C#有用于结构的复制构造函数,这是可能的。可以定义一个包含对真实数据结构的引用的结构,并在该结构的复制构造函数和析构函数中执行所有incremf()/decref()调用

有没有一种方法可以在不打扰API用户的情况下,在C#中自动执行引用计数或结构复制构造函数之类的操作

编辑:

  • 我只是想澄清一下,我只是想问一下API。Clojure已经有了一个用Java编写的实现
  • 当然,通过使用结构并引用每个操作中包含的真实集合来创建这样的接口是可能的。使用重新计数将是一种优化,以避免不必要的畏缩,但显然不可能使用理智的API
您可以使用该类作为重新计数的替代方案,并实现重新计数给您带来的一些好处。当您持有WeakReference中对象的唯一副本时,它将被垃圾收集。WeakReference提供了一些钩子,可供您检查情况是否如此

编辑3:虽然这种方法确实起到了作用,但我建议您不要在C#集合上使用值语义。您的结构的用户不希望在平台上出现这种行为。这些语义增加了混乱和错误的可能性

编辑2:添加了一个示例。@AdamRobinson:恐怕我不清楚
WeakReference
如何使用。我必须警告说,从性能角度来看,大多数情况下,它可能比在每次操作中进行简单的写时复制还要糟糕。这是由于垃圾收集器调用造成的。因此,这仅仅是一个学术解决方案,我不推荐在生产系统中使用它。然而,它确实会按你的要求做

class Program
{

  static void Main(string[] args)
  {
    var l1 = default(COWList);
    l1.Add("foo"); // initialize
    l1.Add("bar"); // no copy
    l1.Add("baz"); // no copy
    var l2 = l1;
    l1.RemoveAt(0); // copy
    l2.Add("foobar"); // no copy
    l1.Add("barfoo"); // no copy
    l2.RemoveAt(1); // no copy
    var l3 = l2;
    l3.RemoveAt(1); // copy
    Trace.WriteLine(l1.ToString()); //  bar baz barfoo
    Trace.WriteLine(l2.ToString()); // foo baz foobar
    Trace.WriteLine(l3.ToString()); // foo foobar
  }
}

struct COWList
{
  List<string> theList; // Contains the actual data
  object dummy; // helper variable to facilitate detection of copies of this struct instance.
  WeakReference weakDummy; // helper variable to facilitate detection of copies of this struct instance.

  /// <summary>
  /// Check whether this COWList has already been constructed properly.  
  /// </summary>
  /// <returns>true when this COWList has already been initialized.</returns>
  bool EnsureInitialization()
  {
    if (theList == null)
    {
      theList = new List<string>();
      dummy = new object();
      weakDummy = new WeakReference(dummy);
      return false;
    }
    else
    {
      return true;
    }
  }

  void EnsureUniqueness()
  {
    if (EnsureInitialization())
    {

      // If the COWList has been copied, removing the 'dummy' reference will not kill weakDummy because the copy retains a reference.
      dummy = new object();

      GC.Collect(2); // OUCH! This is expensive. You may replace it with GC.Collect(0), but that will cause spurious Copy-On-Write behaviour.
      if (weakDummy.IsAlive) // I don't know if the GC guarantees detection of all GC'able objects, so there might be cases in which the weakDummy is still considered to be alive.
      {
        // At this point there is probably a copy.
        // To be safe, do the expensive Copy-On-Write
        theList = new List<string>(theList);
        // Prepare for the next modification
        weakDummy = new WeakReference(dummy);
        Trace.WriteLine("Made copy.");

      }
      else
      {
        // At this point it is guaranteed there is no copy.
        weakDummy.Target = dummy;
        Trace.WriteLine("No copy made.");

      }
    }
    else
    {

      Trace.WriteLine("Initialized an instance.");

    }
  }

  public void Add(string val)
  {
    EnsureUniqueness();
    theList.Add(val);
  }

  public void RemoveAt(int index)
  {
    EnsureUniqueness();
    theList.RemoveAt(index);
  }

  public override string ToString()
  {
    if (theList == null)
    {
      return "Uninitialized COWList";
    }
    else
    {
      var sb = new StringBuilder("[ ");
      foreach (var item in theList)
      {
        sb.Append("\"").Append(item).Append("\" ");
      }
      sb.Append("]");
      return sb.ToString();
    }
  }
}

严格地说,你想要做的是不可能的。您可以通过使用静态函数来进行引用计数,但我知道这不是一个非常好的选择


即使有可能,我也会远离这件事。虽然您描述的语义在Clojure中可能很有用,但值类型和引用类型语义之间的交叉会让大多数C#开发人员感到困惑(可变值类型——或具有可变值类型语义的类型——通常也被认为是邪恶的)。

我读了您的要求,我想到的是“终端服务器”-类型API结构

首先,定义一个内部的线程安全的单例类,它将成为您的“服务器”;它实际上保存了您正在查看的数据。它将公开一个Get-and-Set方法,该方法将获取正在设置或获取的值的字符串,由ReaderWriterLock控制,以确保任何人都可以读取该值,但不能在任何人编写时读取,并且一次只能有一人写入

然后,为作为“终端”的类提供工厂;此类将是公共的,并且包含对内部单例的引用(否则无法看到)。它将包含实际上只是singleton实例的传递的属性。通过这种方式,您可以提供大量的“终端”,这些终端都可以看到来自“服务器”的相同数据,并且能够以线程安全的方式修改这些数据


您可以使用复制构造函数和每个实例访问的值列表来提供复制类型知识。您还可以将值名称与对象句柄混搭,以支持L1和L2共享A,但L3具有不同的A的情况,因为它是单独声明的。或者,L3可以得到与L1和L2相同的A。无论您如何构造它,我都会非常清楚地记录它应该如何运行,因为这不是基本.NET中的行为方式。

我希望在我的灵活树集合对象上有类似的内容,尽管它不会使用值类型语义(这在.NET中基本上是不可能的)但通过让克隆生成“虚拟”深度克隆,而不是实际克隆集合中的每个节点。每个内部节点将有三种状态,而不是试图保持准确的引用计数:

  • 灵活的
  • 共享可调
  • 不可共享的 在SharedMitutable节点上调用Clone()只会产生原始的
    Initialized an instance.
    No copy made.
    No copy made.
    Made copy.
    No copy made.
    No copy made.
    No copy made.
    Made copy.
    [ "bar" "baz" "barfoo" ]
    [ "foo" "baz" "foobar" ]
    [ "foo" "foobar" ]
    
    MyCollection["this"]["that"]["theOther"].Add("George")