【问题标题】:C# (.NET) Objects with dependent life-cycle on owning objectsC# (.NET) 具有依赖于拥有对象的生命周期的对象
【发布时间】:2015-05-28 23:48:53
【问题描述】:

我来自 C++ 背景,想听听 C# (.NET) 专家对以下问题陈述的一些想法,我愿意接受解决方法,但要求被冻结。

问题陈述:

  1. 拥有一个系统,一旦拥有的对象被删除,就会自动清理依赖对象(与下面解释的 GC 提供的有点不同。)

  2. 依赖对象可能有其他引用而不是其拥有对象,但一旦拥有对象被删除,依赖对象就需要离开

  3. 能够用存根对象(占位符)引用替换其他未完成的引用,因为实际对象不再退出

  4. 系统需要与对象无关,并且应该能够检测引用或用从 System.Object (.net) 继承的任何对象的存根替换它们

术语定义:

从属对象:始终需要所有者的对象,但也可能被其他对象引用。然而,依赖对象的生命周期将由拥有对象完全拥有。如果拥有对象被删除,则依赖对象必须被删除。

存根对象 这些是表示已删除引用的对象。

功能背景

为了能够支持功能需求,我们需要一个系统,该系统将自动清理所有者被删除的依赖对象,然后它会用存根替换其他引用以指示它所持有的对象已被删除或卸载,

用一个简单的例子来解释一下

  1. Time T1 - 假设我们创建了一个 Line 对象。由于创建一条线需要一个起点和终点,因此它创建了 2 个点(Pt1 和 Pt2)对象。点对象被标记为从属对象,而线对象是所有者。所以在任何时候,如果我们删除 Line,它应该去删除 Pt1 和 Pt2。

  2. 时间 T2:我们创建两个新点 Pt3 和 Pt4(它们现在是独立的对象)

  3. 时间 T3:我们创建一个正在引用的曲线对象(Pt2、Pt3 和 Pt4)。这里 Pt2 的生命周期由 Line 对象控制。

  4. 时间 T4:我们从图形中删除 Line 对象,现在作为一项要求,此操作必须删除 Pt1 和 Pt2,因为它们是由 Line 创建的,并且 Line 对象已被删除。

  5. 时间 T5:由于曲线也引用了 Pt2,因此现在它的几何计算不完整,将参考存根对象。 Curve 对象将被标记为已损坏,以便在将来的某个时间点我们可以对其进行编辑以引用新点。

拥有这个系统的关键问题是因为删除是由.NET系统控制的,我们无法控制它。任何想法如何在 C# 或 .NET 中实现这一点(在 C++ 中,我们可以完全控制内存管理,因此可以在删除指针之前从指针确定活动引用并在内存中删除或替换它们)。

我了解垃圾收集器有其自身的巨大优势,但这是我们需要在基于 .NET 的 C# 模型中支持的关键要求。

任何想法,建议都表示赞赏。

【问题讨论】:

  • 为什么需要这种精细的控制?
  • 拥有一个系统,一旦拥有的对象被删除,它就会自动清理依赖对象(与下面解释的 GC 提供的有点不同。) 你真的不能在 C# 中强制删除对象。您可以运行GC.Collect() 并希望,但这不是“正确”的做法。 C# 没有确定性的终结/对象集合。你可以拥有的是Dispose(),它可以用来释放对象使用的部分资源(不是对象的“基本内存”)
  • 我给出的示例非常简单......我们现在拥有基于 C++ 的体系结构,但未来我们也将支持基于 .NET 的语言。我们热衷于基于几何和关联的计算软件,我们总是需要一直处理各种情况。如果将它们保存在不同的文件中,则可能会删除或根本不加载依赖关联。
  • 你还没有解释为什么你需要控制它。我推断您不相信垃圾收集器能够快速清除内存(并且您假设它不能满足您的需求,或者您只是对缺乏控制感到恼火。无论哪种方式,如果你想完全控制内存使用,你就不能使用像 .NET 这样的托管框架。最好将你的库保留在 C++ 中并使用C++/CLI 编写一个 .NET API。
  • 替换引用所指对象的唯一方法是修改引用。因此,当您删除线并因此删除点时,您需要修改存储在曲线中的参考。如果您不能/不会这样做,则需要将此类对象包装在可能有内容或没有内容的包装器中。然后曲线将引用它一直引用的同一个包装器,但您告诉包装器忘记内部点。无论哪种方式,这都必须明确地完成,.NET 中没有任何东西可以为您“自动解决”。

标签: c# .net memory-management dependency-management


【解决方案1】:

通常,您无法控制 C# 中的内存释放。正如 Ameya 所建议的,你可以做的是有一个“脏”标志。

是的,我考虑过 Dirty field 方法,但正如我所说,这需要由系统级别管理。如果一个对象被标记为 Dirty 其他对象

请注意,在 .NET 中有很多类可以做到这一点:许多 IDisposable 类(尤其是从 Stream 继承的类!)当 Dispose()d 时,他们将 disposed 标志设置为 @ 987654326@,并在属性/方法中执行if (disposed) throw ObjectDisposedException()。在您的情况下,您不应该这样做,您应该简单地 return;return (some default value);

public class ObjectWithReferences : IDisposable
{
    private List<ObjectWithReferences> childs;

    protected readonly ObjectWithReferences Parent;

    public bool IsDisposed { get; private set; }

    protected ObjectWithReferences(ObjectWithReferences parent)
    {
        Parent = parent;

        if (parent != null)
        {
            parent.AddChild(this);
        }
    }

    private void AddChild(ObjectWithReferences child)
    {
        if (IsDisposed)
        {
            child.Dispose();
            return;
        }

        if (childs == null)
        {
            childs = new List<ObjectWithReferences>();
        }

        childs.Add(child);
    }

    private void DisposeChilds()
    {
        if (childs == null)
        {
            return;
        }

        foreach (ObjectWithReferences child in childs)
        {
            if (!child.IsDisposed)
            {
                child.Dispose();
            }
        }

        childs = null;
    }

    public void Dispose()
    {
        if (!IsDisposed)
        {
            try
            {
                Dispose(true);
            }
            finally
            {
                try
                {
                    DisposeChilds();
                }
                finally
                {
                    IsDisposed = true;
                    GC.SuppressFinalize(this);
                }
            }
        }
    }

    ~ObjectWithReferences()
    {
        if (!IsDisposed)
        {
            try
            {
                Dispose(false);
            }
            finally
            {
                try
                {
                    DisposeChilds();
                }
                finally
                {
                    IsDisposed = true;
                }
            }
        }
    }

    protected virtual void Dispose(bool disposing)
    {
        // Does nothing, not necessary to call!
    }
}

使用示例:

public class ExampleRoot : ObjectWithReferences
{
    public ExampleRoot() : base(null)
    {
    }

    public void Foo()
    {
        if (IsDisposed)
        {
            return;
        }

        // Do Foo things
    }

    public void CreateChild()
    {
        if (IsDisposed)
        {
            return;
        }

        // Auto-adds itself!
        var child = new ExampleChild(this);
    }
}

public class ExampleChild : ObjectWithReferences
{
    private byte[] BigBuffer = new byte[1000000];

    public ExampleChild(ExampleRoot parent) : base(parent)
    {
    }

    protected override void Dispose(bool disposing)
    {
        // The ExampleChild object has a very long possible lifetime,
        // because it will live even in the IsDisposed == true state,
        // so it is better to free even managed resources.
        BigBuffer = null;
    }
}

代码非常简单/清晰...有两个示例类(RootChild)。基本思想是一个“特殊”对象ObjectWithReferences,它保留了其子对象的引用。它是IDisposable,当Dispose() 被调用(或当它最终确定时)它Dispose() 它的所有子对象。您可以使用您的类从该对象继承。您的每个方法/属性都应始终检查IsDisposed 属性以查看对象是否已被释放。如果它已被释放,它们应该什么都不做并返回默认值(0、nullstring.Empty、...)。请注意,如果其中一个对象保留对大型托管对象(例如数组)的引用,则与建议的 .NET 准则相反,它应该 null 这些引用让 GC 收集它们。

请注意,将正在构建的对象添加到其父级的是构造函数!

【讨论】:

  • 感谢 xanatos ...这在基于数学、依赖和计算的数据模型中仍然容易出错,因为它需要引用对象来检查程序员需要手动执行的 Dirty 并且有机会该对象状态即使是脏的也可以更改,因此我们需要系统强制对删除其所有者的依赖对象执行任何数据查询或编辑。如果所有者在场,则允许访问数据。
  • @Ameya 你不应该有公共领域,只有属性。属性可以做检查。 程序员需要手动做的事情这部分是对的,但这是每个方法的责任。请注意,您可以编写一个 Fody 插件来为您完成。已经有类似的东西添加了throw ObjectDisposedException():github.com/Fody/Janitor
  • 是的,同意,我们不打算拥有公共字段,但可以通过属性访问(即使我们检查 get 方法并在对象脏时引发异常)从架构感知这是不够通用,无法被任何对象使用。此外,期望每个引用都开始处理依赖的脏对象也是不可行的。它的对象特定编码的方式。此外,如果依赖对象在撒谎,那么开发人员可以使用反射对其值做任何事情:)...
  • @Ameya 我认为你还没有理解。如果对象 Point 被 Disposed 然后被某人使用,它将什么都不做/返回默认值,因为 Point 对象的每个方法/属性都会检查 this.IsDisposed。请注意,即使在您的分析中,您也写了The Curve object will be marked as broken so that in future point of time we can edit it to refer to new point.,但您没有写出应该如何获得它。 Curve 有两种选择:继续使用 Point 而不检查它是否已释放(如果已释放则采用默认值),或始终检查 Point 是否已释放。
【解决方案2】:

这里的正常做法是使用WeakReference

如果您需要自动存根行为,您可以执行以下操作:

public class AutoStubbed<T> where T:class
{
  private WeakReference<T> _reference;
  private T _stub;
  private readonly Func<T> _stubFactory;
  public AutoStubbed(T value, T stub)
  {
    _reference = new WeakReference<T>(value);
    _stub = stub;
  }
  public AutoStubbed(T value, Func<T> factory)
  {
    _reference = new WeakReference<T>(value);
    _stubFactory = factory;
  }
  public T Target
  {
    get
    {
      T ret;
      if(_reference.TryGetTarget(out ret))
        return ret;
      if(_stub == null && _stubFactory != null)
        _stub = _stubFactory();
      return _stub;
    }
  }
}

然后将T 输入到您的对象和存根定义的接口,而不是对象的类型。

【讨论】:

  • @xanatos 抱歉,这是一个坏主意,我没有完全删除。当你做一些愚蠢的事情时,答案中的代码示例问题不会给你一个编译器错误。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-07-10
  • 1970-01-01
相关资源
最近更新 更多