d来实现qt信号与槽

原文
这样实现类似Qt中的信号和槽:

class Foo : XObject
{
    @signal
    void message( string str );
}
class Bar : XObject
{
    @slot
    void print( string str ) { writefln( "Bar.print: %s", str ); }
}
void main()
{
    auto a = new Foo, b = new Bar;
    connect( a.message, b.print );
    a.message( "你好,habr" ); 
}

最终版本:

class Foo : XObject
{
    mixin MixX; //要嵌入些代码,
    @signal
    void _message( string str ) {}//烦人的规则
}
class Bar : XObject
{
    mixin MixX;
    void print( string str ) { writefln( "Bar.print: %s", str ); }
    //基本上只是方法
}
void main()
{
    auto a = new Foo, b = new Bar;
    connect( a.signal_message, &b.print ); //后面详细
    a.message( "你好,habr" ); 
}

规则,有函数与插件函数冲突时,函数优先.

序号要求
1对象都可有效或无效
2可转移对象无效状态(创建后有效)
3对象可有子对象
4如果不再有效,孩子也不再有效
5不应调用无效对象插槽(无意义)

子对象归父对象,不能在垃集期间管理内存
基本接口:

interface ContextHandler
{
protected:
    void selfDestroyCtx(); //自身析构
public:

    @property
    {
        ContextHandler parentCH(); //父
        ContextHandler[] childCH(); //子列表
    }

    final
    {
        T registerCH(T)( T obj, bool force=true ) //可注册为子对象
            if( is( T == class ) )
        {
            if( auto ch = cast(ContextHandler)obj )
                if( force || ( !force && ch.parentCH is null ) ) //确保,即使有父,也改为自身
                    ...
            return obj;
        }

        T newCH(T,Args...)( Args args ) { return registerCH( new T(args) ); } 
        //或立即创建

        void destroyCtx() 
        {//消灭
            foreach( c; childCH ) 
                c.destroyCtx();//先干掉子
            selfDestroyCtx(); //干掉自身
        }
    }
}

不完整,但足以理解ContextHandler代码.
基本上,它是一棵树.当析构对象时,析构子.插槽:

interface SignalConnector 
{//无模板插槽
    void disconnect( SlotContext );
    void disonnectAll();
}

class SlotContext : ContextHandler 
 {//每个槽都有相同上下文,可能会失效
    mixin MixContextHandler; 
//有易于实现的`插件模板`.
protected:
    size_t[SignalConnector] signals; 
//`插槽连接`到的信号
public:
    void connect( SignalConnector sc ) { signals[sc]++; }
    void disconnect( SignalConnector sc )
    {
        if( sc in signals )
        {
            if( signals[sc] > 0 ) signals[sc]--;
            else signals.remove(sc);
        }
    }
protected:
    void selfDestroyCtx() 
    {//`销毁`上下文时,断开`连接`信号
        foreach( sig, count; signals )
            sig.disconnect(this);
    }
}


interface SlotHandler { SlotContext slotContext() @property; }
//方便函数

class Slot(Args...) 
{
protected:
    Func func; //函数
    SlotContext ctrl; 
public:
    alias Func = void delegate(Args);
    this( SlotContext ctrl, Func func ) { this.ctrl = ctrl; this.func = func; }
    this( SlotHandler hndl, Func func ) { this( hndl.slotContext, func ); }
    void opCall( Args args ) { func( args ); }
    SlotContext context() @property { return ctrl; }
}

信号:

class Signal(Args...) : SignalConnector, ContextHandler
{
    mixin MixContextHandler;
protected:
    alias TSlot = Slot!Args;
    TSlot[] slots; //连接的插槽
public:
    TSlot connect( TSlot s )
    {
        if( !connected(s) )
        {
            slots ~= s;
            s.context.connect(this);
        }
        return s;
    }
    void disconnect( TSlot s ) 
    {//可断开
        slots = slots.filter!(a=>a !is s).array;
        s.context.disconnect(this);
    }
    void disconnect( SlotContext sc ) 
    {//整个环境
        foreach( s; slots.map!(a=>a.context).filter!(a=> a is sc) )
            s.disconnect(this);
        slots = slots
            .map!(a=>tuple(a,a.context))
            .filter!(a=> a[1] !is sc)
            .map!(a=>a[0])
            .array;
    }
    void disconnect( SlotHandler sh ) { disconnect( sh.slotContext ); }
    void disonnectAll() 
    {//断开
        slots = [];
        foreach( s; slots ) s.context.disconnect( this );
    }
    
    void opCall( Args args ) { foreach( s; slots ) s(args); }
//调用触发`调用`槽
protected:
    bool connected( TSlot s ) { return canFind(slots,s); }
    void selfDestroyCtx() { disonnectAll(); } 
}//销毁时要断开链接

最后,来到最有趣的部分:XBase接口和XObject中间类(插件MixX并创建了默认构造器).XBase接口仅用几个函数扩展了ContextHandler,最重要的是插件 MixX.这就是元编程神奇的地方.
首先,解释动作逻辑.@signal用定属标记,应是创建真实信号信号对象本身的基础函数.几乎所有内容都取自标记函数:名(没有初始下划线),访问级别(公,保护),当然还有参数.
这些属性中,只允许用@system,因为希望信号可与插槽一起用.实际信号函数传入参数并调用对应信号对象的opCall.
为了不在每个新类中创建信号对象,在MixX中实现了个函数来这样.为什么要创建单独信号函数和信号对象?是为了使信号成为函数,这很奇怪.
这样允许在继承XObject或实现XBase的类中实现接口,及连接信号来调用其他信号:

    interface Messager { void onMessage( string ); }//只有抽象方法可以发送信号
    class Drawable { abstract void onDraw(); } 
    class A : Drawable, XBase
    {
        mixin MixX;
        this() { prepareXBase(); } 
        //创建需要的一切
        @signal void _onDraw() {}
    }
    class B : A, Messager
    {
        mixin MixX;
        @signal void _onMessage( string msg ) {}
    }
    class Printer : XObject
    {
        mixin MixX;
        void print( string msg ) { }
    }

    auto a = new B;
    auto b = new B;
    auto p = new Printer;

    connect( a.signal_onMessage, &b.onMessage ); //连接信号到信号
    connect( &p.print, b.signal_onMessage ); 
...

回到XBase.逐段分解代码:

interface XBase : SlotHandler, ContextHandler
{
public:
    enum signal; //不能在`UDA`中使用不存在的标识符,所以简单用`enum`声明
protected:
    void createSlotContext();
    void createSignals();

    final void prepareXBase() 
    {//必须在实现XBase类构造器中调用此函数
        createSlotContext();
        createSignals();
    }

    
    final auto newSlot(Args...)( void delegate(Args) f ) { return newCH!(Slot!Args)( this, f ); }
    //XBase还扩展了`SlotHandler`,所以可作为创建槽的基础
    //可立即连接`闭包`到信号,责任留给调用此方法的对象
    final auto connect(Args...)( Signal!Args sig, void delegate(Args) f )
    {
        auto ret = newSlot!Args(f);
        sig.connect( ret );
        return ret;
    }

    mixin template MixX()
    {
        import std.traits;
        //使用`C++`中的技术,因为`插件`模板不是模块,我们也可以抓冲突
        static if( !is(typeof(X_BASE_IMPL)) )
        {
            enum X_BASE_IMPL = true;

            mixin MixContextHandler; //
            private SlotContext __slot_context;
            final
            {
                public SlotContext slotContext() @property { return __slot_context; }
                protected void createSlotContext() { __slot_context = newCH!SlotContext; }
            }
        }
        mixin defineSignals; //收集所有信号函数和对象时,会插入这段代码.
        override protected
        {//抽象,则第1次插件.
            static if( isAbstractFunction!createSignals )
                void createSignals() { mixin( mix.createSignalsMixinString!(typeof(this)) ); }
            else 
                void createSignals()
                {//调用基类createSignals
                    super.createSignals();
                    mixin( mix.createSignalsMixinString!(typeof(this)) );
                }//从类型中收集所有已创建了信号并返回串
        }
    }
...
}

mix是一种集中了所有使用方法的结构.这可能不是最好的解决方案,但它允许减少最终类名数,同时保持所有内容在正确的位置(在XBase接口中).考虑:

    static struct __MixHelper
    {
        import std.algorithm, std.array;
        enum NAME_RULE = "必须'_'开头";
//信号模板名只能以`下划线`开头
    static pure @safe:
        bool testName( string s ) { return s[0] == '_'; }
        string getMixName( string s ) { return s[1..$]; }
//此函数生成创建信号对象的`串和信号`函数
        string signalMixinString(T,alias temp)() @property
        {
            ...
        }
        enum signal_prefix = "signal_";
        //信号前缀.
        string createSignalsMixinString(T)() @property
        {
            auto signals = [ __traits(derivedMembers,T) ]
                .filter!(a=>a.startsWith(signal_prefix)); //前缀名.

            return signals
                .map!(a=>format("%1$s = newCH!(typeof(%1$s));",a)) 
                .join("\n");
        }//创建信号时,按子对象加.

        template functionFmt(alias fun) if( isSomeFunction!fun )
        {//错误输出,实用函数
            enum functionFmt = format( "%s %s%s",
                (ReturnType!fun).stringof, 
                //返回类型,名字,参数列表.
                __traits(identifier,fun), 
                (ParameterTypeTuple!fun).stringof ); 
        }
    }

    protected enum mix = __MixHelper.init;

回到MixX,其中最难的是不变的mixin defineSignals.

    mixin template defineSignals() { mixin defineSignalsImpl!( typeof(this), getFunctionsWithAttrib!( typeof(this), signal ) ); }
 //通过`defineSignalsImpl`得到有`@signal`属性的函数
    mixin template defineSignalsImpl(T,list...)
    {//函数列表
        static if( list.length == 0 ) {} //空
        else static if( list.length > 1 )
        {//分而治之.
            mixin defineSignalsImpl!(T,list[0..$/2]);
            mixin defineSignalsImpl!(T,list[$/2..$]);
        }
        else mixin( mix.signalMixinString!(T,list[0]) ); //插入声明`信号函数和对象`的串
    }

getFunctionsWithAttrib模板和mix.signalMixinString一样复杂.先看后者.

        string signalMixinString(T,alias temp)() @property
        {
            enum temp_name = __traits(identifier,temp); //取信号模板函数名
            enum func_name = mix.getMixName( temp_name ); //取信号函数名
            //模板函数,只允许`@system`属性
            enum temp_attribs = sort([__traits(getFunctionAttributes,temp)]).array;
            static assert( temp_attribs == ["@system"],format( "fail Mix X for '%s': 仅允许@system属性", T.stringof ) );
            static if( __traits(hasMember,T,func_name) )//检查是否声明同名函数
            {
                alias base = AT!(__traits(getMember,T,func_name)); //仔细考虑
                static assert( isAbstractFunction!base,format( "fail Mix X for '%s': target signal function '%s' must be abstract in base class",T.stringof, func_name ) );
                //它应抽象的

                
                enum base_attribs = sort([__traits(getFunctionAttributes,base)]).array;//只能有@system属性
                static assert( temp_attribs == ["@system"],
                        format( "fail Mix X for '%s': 只允许@system属性", T.stringof ) );

                enum need_override = true;
            }
            else enum need_override = false;

            enum signal_name = signal_prefix ~ func_name;//除了信号声明外,还为信号`参数类型`元组创建了别名,因此以后更易调用信号
            enum args_define = format( "alias %sArgs = ParameterTypeTuple!%s;", func_name, temp_name );

            enum temp_protection = __traits(getProtection,temp); //形成与`模板函数`具相同`可访问性`的信号对象声明.
            enum signal_define = format( "%s Signal!(%sArgs) %s;", temp_protection, func_name, signal_name );
            //立即用主体形成`信号`函数声明,在其中调用信号对象的`opCall`
            enum func_impl = format( "final %1$s %2$s void %3$s(%3$sArgs args) { %4$s(args); }",(need_override  "override" : ""), temp_protection, func_name, signal_name );
            return [args_define, signal_define, func_impl].join("\n");//格式化为几行.
        }

取标记函数列表.

    template getFunctionsWithAttrib(T, Attr)
    {//重要,只取那些在需要调用`base`的T类中明确声明`字段和方法`对象信号创建
        alias getFunctionsWithAttrib = impl!( __traits(derivedMembers,T) );
        //`std.typetuple`有使使用类型元组更容易的函数
        enum AttrName = __traits(identifier,Attr);//这样的模板可以在`staticMap`和/或`anySatisfy`中使用
        template isAttr(A) { template isAttr(T) { enum isAttr = __traits(isSame,T,A); } }
        template impl( names... )
        {//再次使用函数式样式
            alias empty = TypeTuple!();

            static if( names.length == 1 )
            {
                enum name = names[0];
                //不是可别名所有`__traits(derivedMembers,T)`返回的东西,例如,有些`this`不是一个字段,所以不能获取它
                static if( __traits(compiles, { alias member = AT!(__traits(getMember,T,name)); } ) )
                {//不能直接写 alias some = __traits(...),不方便.
                    alias member = AT!(__traits(getMember,T,name));//因而用`template AT(alias T) { alias AT = T; }`
                    alias attribs = TypeTuple!(__traits(getAttributes,member));//同样,但不止一个
                    static if( anySatisfy!( isAttr!Attr, attribs ) )
                    {//如果至少需要一个属性
                        enum RULE = format( "%s 必须是个空函数", AttrName );
                        //检查是否是函数
                        static assert( isSomeFunction!member,format( "fail mix X for '%s': %s, found '%s %s' with @%s attrib",T.stringof, RULE, typeof(member).stringof, name, AttrName ) );
                        static assert( is( ReturnType!member == void ), format( "fail mix X for '%s': %s, found '%s' with @%s attrib", T.stringof, RULE, mix.functionFmt!member, AttrName ) );
                    //信号函数只能是 void
                    static assert( mix.testName( name ), format( "fail mix X for '%s': @%s name %s", T.stringof, mix.functionFmt!member, AttrName, mix.NAME_RULE ) );

                    alias impl = member; 
                    }
                    else alias impl = empty;
                }
                else alias impl = empty;
            }
            else alias impl = TypeTuple!( impl!(names[0..$/2]), impl!(names[$/2..$]) );
        }//impl最后返回结果.
    }//可以得知任务插件

连接功能:

void connect(T,Args...)( Signal!Args sig, T delegate(Args) slot )
{
    auto slot_handler = cast(XBase)cast(Object)(slot.ptr); 
    //基本上是肮脏的修改
    enforce( slot_handler, "槽环境不是XBase" );
    static if( is(T==void) ) slot_handler.connect( sig, slot );
    //因为`slot`可以是任意函数,除非函数是`void`,将忽略结果
    else slot_handler.connect( sig, (Args args){ slot(args); } );
}
void connect(T,Args...)( T delegate(Args) slot, Signal!Args sig ) { connect( sig, slot ); }

为何不改信号?如要像文章开头那样调用connect:

connect( a.message, b.print );

首先,此时,需要固定信号和槽顺序,这应反映在名称中.但最重要原因是:它不起作用.这样:

void connect!(alias sig,alias slot)()...

不允许保存上下文,别名本质上是Class.method,其中Class类名,而不是对象.需要添加.检查信号和槽参数匹配.带闭包.

void connect(T,Args...)( void delegate(Args) sig, T delegate(Args) slot ) { ... }

connect( &a.message, &b.print );

丢失有关包含信号类信息.我未找到使用(sig.funcptr)函数指针显示名称方法,且要按某种方式构建信号对象的名称,并从字典中返回(SignalConnector[string]).看起来不是很好.

posted @   zjh6  阅读(45)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示