是否存在一种通用模式可以通过自动状态离开操作在 C# 中为对象提供表达式或状态生命周期?

发布于 2024-12-02 00:33:59 字数 2906 浏览 4 评论 0原文

命令式应用程序中有四种基本的对象生命周期:

  1. 表达式(临时)生命周期
  2. 范围生命周期
  3. 状态生命周期(事件之间)
  4. 应用程序生命周期

c# 的设计没有 RAII 支持,后来添加了 using 语句作为提供自动化必要机制的一种手段作用域的生命周期。应用程序生命周期是托管应用程序范围,因为应用程序关闭与调用垃圾收集一样具有确定性。这使得表达式和状态生命周期无法通过任何自动确定性破坏的语言机制来处理。

C# 中是否有解决这些基本需求的通用模式?我确信这些针对常见需求的解决方案现在已经被解决了很多次,但是在网上查找,我找不到任何关于此的文章。

在 C++ 中,临时生命周期对象中的 dtor 用于为表达式提供代理或包装器,并且可以在各种用于修改表达式、临时更改流状态以及表达式模板等高级优化的习惯用法中找到。对于状态,常见的面向对象解决方案是使用状态模式,并将具有给定状态生命周期的对象作为成员放入状态对象中。通过这种方式,例如,如果屏幕上有一个给定状态的显示框,ctor 将显示该对象,而 dtor 将从显示系统中删除它。

当我在线搜索“表达式模板”或“状态模式”之类的内容时,我得到的结果不包括自动终结器调用。相反,我看到对清理状态的调用作为单独的函数调用留下(因此,如果您有一个显示对象的多个状态,如示例所示,您必须在每个状态中调用清理,而不是只编写一次清理代码在对象中,并且类似地记住每个状态的所有对象)。我看到将单个表达式分解为多个语句以合并临时变量的显式终结。

对于临时工,我通常倾向于

(/*expression that builds proxy at some point*/).Dispose();

支持多线解决方案。

对于最近的状态生命周期项目,我创建了以下类

namespace libAutoDisposable
{
    public class AutoDisposable
    {
        public void AutoDispose()
        {
            // use reflection to get the fields of this
            FieldInfo[] infos = GetType().GetFields();

            // loop through members
            foreach (FieldInfo info in infos)
            {
                // now try to call AutoDispose or Dispose on each member
                if (typeof(AutoDisposable).IsAssignableFrom(info.FieldType))
                {
                    // get the value object
                    AutoDisposable value = (AutoDisposable)info.GetValue(this);

                    // and invoke
                    value.AutoDispose();
                }
                else if (typeof(IDisposable).IsAssignableFrom(info.FieldType))
                {
                    // get the value object
                    IDisposable value = (IDisposable)info.GetValue(this);

                    // so invoke
                    value.Dispose();
                }
            }
        }
    }
}

,它将迭代状态对象的成员,并且当调用状态对象的 AutoDispose 时,所有需要终结的对象(即从 IDisposable 派生)将调用 Dispose。此外,我将其递归化以支持对象重用。这样就不需要在每个状态中编写清理代码,而是允许我的状态机在转换代码中调用一次 AutoDispose。

然而,这有很多缺点,包括:

  • 使对象在遇到异常时未被回收
  • 确定每次状态转换时在运行时调用的方法,而不是每个类(希望如此)运行时或(世界上最好的)翻译时调用一次。
  • 基类反射方法是一种尽可能黑客/侵入性的代码。

我确信优秀的架构师已经在 C# 项目中工作过并且必须解决这些问题。哪些模式已经发展到可以自动破坏表达和状态生命周期?


编辑:我写了我的状态解决方案的缺点,但我忘记提及为什么我发现我倾向于使用的表达式或临时生命周期解决方案不令人满意。从域语言的角度来看,对代理上的 Dispose 的调用将表达式标识为具有代理。不过,通常情况下,代理的意义在于,有些表达式带有它们,有些则没有它们,并且表达式在某个时刻是否返回代理是一个实现细节。例如,您可能

mySerialisationStream.Serialise(obj1).Serialise(obj2);

mySerialisationStream.Serialise(specialFormatter).Serialise(obj1).Serialise(obj2);

插入一个特殊的格式化程序,该格式化程序持续 Serialise 调用行的长度,然后返回默认格式。必须添加 Dispose 调用意味着我知道一行何时具有特殊的格式化程序对象,尽管我可能会尝试一般性地执行操作。然后,当只有代理需要采取任何操作时,我要么必须重新设计以向 MySerializationStream 类添加一个不执行任何操作的 Dispose。随着我在表达式中使用的类型数量的增加,这会增加组合的复杂性。

There are four basic lifetimes of objects in imperative applications:

  1. Expression (temporary) lifetime
  2. Scope lifetime
  3. State lifetimes (between events)
  4. Application lifetime

c# was infamously designed without RAII support, and later added the using statement as a means of providing the necessary mechanism for automating scope lifetimes. Application lifetimes are managed application scope because application close is about as deterministic as you can get for invoking garbage collection. This leaves expression and state lifetimes unhandled with any language mechanism for automated deterministic destruction.

Are there common patterns that solve these fundamental needs in c#? I am sure that these kinds of solutions to a common need have been solved a number of times by now, but looking online, I cannot find any articles on this.

In c++, dtors in temporary lifetime objects are used to provide proxies or wrappers for expressions, and are found in a variety of idioms for modfying expressions, altering stream states temporarily, and advanced optimisations like expression templates. For states, the common OO solution is to use the State pattern and to put objects with the lifetime of a given state inside the state object as members. In this way, for instance, if you have a display box on the screen for a given state, the ctor will display the object and the dtor will remove it from the display system.

When I do a search for things like "expression templates" or "state pattern" online, the results I get do not include automated finaliser calls. Instead, I see the calls to cleanup a state left as a separate function call (so if you have multiple states with a display object, as the example goes, you have to call cleanup in each state, instead of just writing the cleanup code once in the object, and similarly remember this for all objects of each state). And I see what would be single expressions broken up into multiple statements to incorporate the explicit finalisation of temporaries.

For temporaries, I have typically leaned towards

(/*expression that builds proxy at some point*/).Dispose();

in favor of multiline solutions.

For a recent project for state lifetimes, I made the following class

namespace libAutoDisposable
{
    public class AutoDisposable
    {
        public void AutoDispose()
        {
            // use reflection to get the fields of this
            FieldInfo[] infos = GetType().GetFields();

            // loop through members
            foreach (FieldInfo info in infos)
            {
                // now try to call AutoDispose or Dispose on each member
                if (typeof(AutoDisposable).IsAssignableFrom(info.FieldType))
                {
                    // get the value object
                    AutoDisposable value = (AutoDisposable)info.GetValue(this);

                    // and invoke
                    value.AutoDispose();
                }
                else if (typeof(IDisposable).IsAssignableFrom(info.FieldType))
                {
                    // get the value object
                    IDisposable value = (IDisposable)info.GetValue(this);

                    // so invoke
                    value.Dispose();
                }
            }
        }
    }
}

which would iterate over the members of a state object, and all objects needing finalisation (ie. deriving from IDisposable) would call Dispose when the state object's AutoDispose was called. Additionally, I made it recursive to support object reuse. This prevents the need for having to write cleanup code in each state, instead allowing my state machines to call AutoDispose a single time in the transition code.

However, this has a number of downsides, including:

  • Leaves objects unreclaimed in the face of exceptions
  • Determines the methods to call at runtime each time a state transitions, instead of once per class (hopefully) runtime or (best-of-worlds) translationtime.
  • The base class reflection approach is about as hackish / intrusive code as one can get.

I'm sure good architects out there have worked on projects in c# and had to solve these issues. What patterns have evolved to give expression and state lifetimes automated destruction?


EDIT: I wrote about the shortcomings of my state solution, but I forgot to mention why I find the expression or temporary lifetime solution I tend to use unsatisfactory. From a domain language point of view, the call to Dispose on the proxy identifies the expression as having the proxy. Usually, the point of the proxy, though, is because there are some expressions with them and some without them, and whether or not the expression at some point returns a proxy is an implementation detail. As an example, you might have

mySerialisationStream.Serialise(obj1).Serialise(obj2);

and

mySerialisationStream.Serialise(specialFormatter).Serialise(obj1).Serialise(obj2);

The second case might insert a special formatter that lasts the length of the line of Serialise calls and then returns to default formatting. Having to add a Dispose call means I know when a line has a special formatter object, though I may be trying to do things generically. I then either have to redesign to add a do-nothing Dispose to MySerialisationStream class, when only the proxy needs to take any action. This adds combinatorial complexity as the number of types I'm working with in an expression increases.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

扛刀软妹 2024-12-09 00:33:59

您可以将 using 语句与代码块一起“包装”为函数调用中的 lambda 表达式,以实现 RAII 语义:

        var stringBuilder = new StringBuilder();
        var stream = new Stream(stringBuilder);

        stream
            .Serialize(1)
            .IsolateMemento(s=>new StreamMemento(s),s=>s
                .Serialize(new Formatter("formatted {0}"))
                .Serialize(2))
            .Serialize(3);

        Assert.AreEqual("1;formatted 2;3;", stringBuilder.ToString());

主要部分是以下扩展方法:

    public static class MementoExtensions
    {
        public static T IsolateMemento<T>(
            this T originator,
            Func<T, IDisposable> generateMemento,
            Func<T, T> map)
        {
            using (generateMemento(originator))
                return map(originator);
        }
    }

实现细节:

    public class Stream
    {
        public StringBuilder StringBuilder { get; set; }
        public Stream(StringBuilder stringBuilder) { StringBuilder = stringBuilder; }
        public Formatter Formatter = new Formatter("{0}");
        public Stream Serialize(object o)
        {
            var formatter = o as Formatter;
            if (formatter != null) Formatter = formatter;
            else StringBuilder.Append(Formatter.Format((o ?? "").ToString())).Append(";");
            return this;
        }
    }

    public class Formatter
    {
        public string FormatString { get; set; }
        public Formatter(string s) { FormatString = s; }
        public string Format(string s) { return string.Format(FormatString, s); }
    }

    public class StreamMemento : IDisposable
    {
        private Stream Originator { get; set; }
        private Formatter FormatterBefore { get; set; }
        public StreamMemento(Stream s) { Originator = s; FormatterBefore= s.Formatter; }
        public void Dispose() { Originator.Formatter = FormatterBefore; }
    }

更新:

如果您想使用装饰器,例如从 Serialize 返回装饰器方法,可以使用另一个扩展:

public static T DecoratedAction<T>(
                this T originator,
                Func<T,T> createDecorator,
                Action<T> act)
            {
                var decorated = createDecorator(originator);
                act(decorated);
                return originator;
            }

用法是:

stream.Serialize(obj1).DecoratedAction(s=>s.Serialize(formatter), s=>s.Serialize(obj2)).Serialize(obj3)

当 Serialize(formatter) 返回装饰代理 Stream 时,功能等同于:

stream.Serialize(obj1).DecoratedAction(s=>new FormattedStream(formatter,s), s=>s.Serialize(obj2)).Serialize(obj3)

请注意,装饰器和备忘录都是在 lambda 表达式中创建的,因为我不想要另一个该表达式中stream的入口 - 它必须能够对调用链中先前表达式的结果进行操作,这对于构建流畅的接口至关重要。

我在这里没有使用 using/IDisposable ,因为我假设装饰器并不意味着被确定性地处置。此外,如果返回代理或原始对象,这种方式并不重要。

You can 'wrap' using statement along with a code block as lambda expression in a function call to achieve RAII semantics:

        var stringBuilder = new StringBuilder();
        var stream = new Stream(stringBuilder);

        stream
            .Serialize(1)
            .IsolateMemento(s=>new StreamMemento(s),s=>s
                .Serialize(new Formatter("formatted {0}"))
                .Serialize(2))
            .Serialize(3);

        Assert.AreEqual("1;formatted 2;3;", stringBuilder.ToString());

The main part is following extension method:

    public static class MementoExtensions
    {
        public static T IsolateMemento<T>(
            this T originator,
            Func<T, IDisposable> generateMemento,
            Func<T, T> map)
        {
            using (generateMemento(originator))
                return map(originator);
        }
    }

Implementation details:

    public class Stream
    {
        public StringBuilder StringBuilder { get; set; }
        public Stream(StringBuilder stringBuilder) { StringBuilder = stringBuilder; }
        public Formatter Formatter = new Formatter("{0}");
        public Stream Serialize(object o)
        {
            var formatter = o as Formatter;
            if (formatter != null) Formatter = formatter;
            else StringBuilder.Append(Formatter.Format((o ?? "").ToString())).Append(";");
            return this;
        }
    }

    public class Formatter
    {
        public string FormatString { get; set; }
        public Formatter(string s) { FormatString = s; }
        public string Format(string s) { return string.Format(FormatString, s); }
    }

    public class StreamMemento : IDisposable
    {
        private Stream Originator { get; set; }
        private Formatter FormatterBefore { get; set; }
        public StreamMemento(Stream s) { Originator = s; FormatterBefore= s.Formatter; }
        public void Dispose() { Originator.Formatter = FormatterBefore; }
    }

UPDATE:

If you want to use decorators, for example returning decorator from Serialize method, another extension can be used:

public static T DecoratedAction<T>(
                this T originator,
                Func<T,T> createDecorator,
                Action<T> act)
            {
                var decorated = createDecorator(originator);
                act(decorated);
                return originator;
            }

A usage would be:

stream.Serialize(obj1).DecoratedAction(s=>s.Serialize(formatter), s=>s.Serialize(obj2)).Serialize(obj3)

when Serialize(formatter) returns a decorated proxy Stream, functionally equally to:

stream.Serialize(obj1).DecoratedAction(s=>new FormattedStream(formatter,s), s=>s.Serialize(obj2)).Serialize(obj3)

Note that decorator and memento both created in a lambda expression, because i don't want another entrance of stream in this expression - it must be able to operate on a result of previous expression in call chain, which is vital for building fluent interfaces.

I'm not using using/IDisposable here, because i assume decorators are not meant to be deterministically disposed. Also, it does not matter this way, if a proxy or the original object is returned.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文