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])
.看起来不是很好.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现