【问题标题】:Dealing with Immutable disposable objects处理不可变的一次性对象
【发布时间】:2021-11-28 10:04:05
【问题描述】:

给定一个 Disposable Immutable 类,该类有时包含一些很大的东西,并且您不知道当对象被释放两次时是否存在副作用或异常,(而且我不持有将其修改为的代码所有权考虑这种情况)如何处理链式转换的最佳方法是什么?

以位图为例。

        public static Bitmap Transform(Bitmap src, RotateFlipType rotate = RotateFlipType.RotateNoneFlipNone, 
                double scale = 0, int pad = 0, int alterGradient = 0)
        {
            using Bitmap rotated = src.Rotate(rotate);
            using Bitmap scaled = MyImageUtils.ScaleBitmap(rotated, scale);
            using Bitmap padded = MyImageUtils.PaddBitmap(scaled, scale);
            //The owner is the caller
            Bitmap result = MyImageUtils.Gradient(padded, alterGradient);
            return result;
        }

如果您需要使用转换创建一个新位图,那么占用该内存是有意义的,但如果转换没有效果(RotateFlipNone、scale = 0 或 pad = 0),那么创建一个没有意义新位图。我发现自己创建克隆是为了在每次转换时返回一个新的 Disposable 对象,而不是返回相同的输入对象。

例如,如果 Date 对象是 Disposable 并且您需要执行 n 次操作,则同样的情况也适用于该对象,其中一些操作无效,具体取决于输入参数(添加零天)。

关键是,某些操作对输入参数没有影响,创建新对象仍然比跟踪哪个用户是对象的第一个所有者并事先了解您正在使用的 API 更容易如果某些参数真的会创建一个不同的项目或只是一个副本。

  • 这种情况有规律吗?
  • using 是否考虑到它持有的对象引用属于另一个 using,因此它不会释放它两次或抛出 ObjectDisposedException?
  • 是否每次都拥有一个新对象是最安全的方法,即使它需要更多的计算和内存? (从我的角度来看,它看起来是最易读的)

我想到的一个选择是有一个 Disposable 包装类,以确保它持有的对象不会被释放两次,但这意味着我需要事先知道转换是否具有零效应,所以我不会' t 调用它或转换函数知道此包装器机制。 比如:

    public class DisposableOnce<T> : IDisposable
        where T : IDisposable
    {
        private bool disposedValue;

        public delegate void DisposedDelegate(EventArgs e);
        public event DisposedDelegate? OnDisposed;

        public T Value { get; }
        private readonly DisposableOnce<T>? Other;
        public DisposableOnce(T value)
        {
            Value = value;
        }

        public DisposableOnce(DisposableOnce<T> disposableOther)
        {
            Value = disposableOther.Value;
            Other = disposableOther;
            Other.OnDisposed += OnRefDisposed;
        }

        private void OnRefDisposed(EventArgs e)
        {
            SetDisposed();
        }

        public void SetDisposed()
        {
            disposedValue = true;
            try
            {
                OnDisposed?.Invoke(new EventArgs());
            }
            catch (Exception ex)
            {
                //Shallow the exception to avoid propagation?
                throw ex;
            }
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    Value.Dispose();
                    if (Other != null)
                    {
                        //Not listening you anymore
                        Other.OnDisposed -= OnRefDisposed;
                    }
                }
                SetDisposed();
            }
        }

        public void Dispose()
        {
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }
    }

它会像这样使用:

        public static Bitmap Transform(Bitmap src, RotateFlipType rotate = RotateFlipType.RotateNoneFlipNone, double scale = 0, int pad = 0, int alterGradient = 0)
        {
            using DisposableOnce<Bitmap> rotated = new DisposableOnce<Bitmap>(src.Rotate(rotate));
            using DisposableOnce<Bitmap> scaled = scale == 0 ? new DisposableOnce<Bitmap>(rotated) : new DisposableOnce<Bitmap>(MyImageUtils.ScaleBitmap(rotated.Value, scale));
            using DisposableOnce<Bitmap> padded = pad == 0 ? new DisposableOnce<Bitmap>(scaled) : new DisposableOnce<Bitmap>(MyImageUtils.PaddBitmap(scaled.Value, scale));
            Bitmap result;
            if (alterGradient == 0)
            {
                //Avoid the value being disposed by the wrapper relatives
                padded.SetDisposed();
                result = padded.Value;
            }
            else
            {
                result = MyImageUtils.Gradient(padded.Value, alterGradient);
            }
            return result;
        }

这要大得多,令人困惑,需要对每个变换函数有更多的了解(+ 大量的 nono 原因)。

我最好的猜测是保持初始转换,除非存在真正的性能问题,但想知道是否存在一些优雅的解决方案:

  • 一个函数,有时返回一个新实例,有时返回给定的 IDisposable 输入参数本身。
  • 而不是总是返回一个新实例以避免两次处理

【问题讨论】:

  • 需要担心的不是两次处理(这应该不会导致问题,因为 Dispose() 的文档声明了If an object's Dispose method is called more than once, the object must ignore all calls after the first one. The object must not throw an exception if its Dispose method is called multiple times.)。需要担心的是在对象被释放后使用它。
  • 如果 C# 支持它,我建议让转换始终使用输入对象——输入将被释放,或者将在输出中重新使用。如果有人想保留输入的副本,他们会在转换消耗之前制作该副本。但是 C# 对此没有语言支持,所以我认为这不是一个好方法。我想我会尝试真的很努力不要有一次性的不可变对象。处置毕竟需要可变性 - 您正在通过处置它来改变对象的状态。
  • @MatthewWatson 给定内联使用,(我不知道优化,但我可以猜测如果没有进一步的“可见”引用,则该对象将被释放,因为编译器会忽略返回的对象可能下一个 using 语句也是一样的。对于这种情况,嵌套 using with { } 会起作用(但看起来嵌套很深)
  • @canton7 如果输入参数可以是 ref,那么您可以将引用复制到新变量并将参数设置为 null,但 using 语句不允许 ref 或 out。

标签: c# design-patterns immutability disposable


【解决方案1】:

我认为一次性对象并不是真正不可变的。如果它们在哪里,则返回原始对象或新对象不会有任何问题。由于它不是一成不变的,我认为您应该始终做同样的事情,并且永远不要尝试通过返回原始对象来进行优化。

这种情况有规律吗?

有一些模式可以减少创建新对象的影响

  1. 就地修改对象 - 这可能是开销最小的最简单选项。但它会使代码更难理解,而且可能并不总是适用。
  2. 对象池 - 保留可重复使用的图像列表,以避免创建对象的开销。这可以实现“冰棒不变性”,您可以在其中修改对象然后将其冻结,仅在对象返回池时才解冻。
  3. 让调用者提供对象以将结果写入 - 这将对象分配的责任从方法转移到调用者,调用者应该能够更好地决定最佳策略。李>

是否使用保留帐户它持有的对象引用属于另一个使用,因此它不会释放它两次或抛出 ObjectDisposedException?

不,using 只是 try{....} finally{myObject.Dispose()} 的简写。但是,设计良好的对象不应该关心它们是否被处理两次。如果某些第三方对象不遵循此做法,则使用包装器可能是合理的。但是大多数对象不应该需要这样的包装器,我会让这样的包装器比你建议的简单得多,因为它只需要一个标志来判断它是否已经被释放。

是否每次都拥有一个新对象是最安全的方法,即使它需要更多的计算和内存? (从我的角度来看,它看起来是最易读的)

我认为不可变对象更易于使用和理解,这需要创建新对象而不是改变它。但是,它确实会对性能产生影响。这种影响是否显着取决于具体的应用。在执行图像处理之类的事情时,易用性比性能更重要。

【讨论】:

  • +1,完全同意。在进行计算机视觉时,我经常最终手动管理一个大小正确的图像缓冲区池,并在图像处理管道的每个步骤中重复使用这些缓冲区。你,调用者,需要对你的生命周期管理非常明确,这很痛苦,但你得到的表现是值得的。这只有在您使用的库允许您传入缓冲区以写入(此答案中的第 3 点)时才有可能——有时会提供一个帮助方法来为您创建一个新的缓冲区以方便您
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-04-29
  • 1970-01-01
相关资源
最近更新 更多