tolua源码分析(六) C#委托使用lua函数的机制实现

news2024/11/22 14:19:59

tolua源码分析(六) C#委托使用lua函数的机制实现

上一节我们讨论lua层如何使用C#的enum以及使用enum需要注意的事项。这一节我们将讨论项目日常开发中经常会用到的委托注册机制,即C#层暴露若干委托和事件,然后在lua层进行注册,为它们绑定lua层的函数,使得C#层可以通过委托和事件调用到lua函数。这个最常见的用法要数UI了,例如C#的UI Button上有个ActionClick的委托,它会在Button被玩家按下的时候触发,而触发后具体执行的逻辑我们希望在lua层实现,因此我们就需要在lua层,往这个C#委托上绑定函数。那么,这个机制具体是怎样实现的呢?在这便利的背后又隐藏了哪些陷阱呢?还是老样子,我们先从官方给的例子example 11看起,首先是测试用的lua代码:

function DoClick1(go)                
    print('click1 gameObject is '..go.name)                    
end

function DoClick2(go)                
    print('click2 gameObject is '..go.name)                              
end                       

function AddClick1(listener)       
    if listener.onClick then
        listener.onClick = listener.onClick + DoClick1                                                    
    else
        listener.onClick = DoClick1                      
    end                
end

function AddClick2(listener)
    if listener.onClick then
        listener.onClick = listener.onClick + DoClick2                      
    else
        listener.onClick = DoClick2
    end                
end

function SetClick1(listener)
    if listener.onClick then
        listener.onClick:Destroy()
    end

    listener.onClick = DoClick1         
end

function RemoveClick1(listener)
    if listener.onClick then
        listener.onClick = listener.onClick - DoClick1      
    else
        print('empty delegate')
    end
end

function RemoveClick2(listener)
    if listener.onClick then
        listener.onClick = listener.onClick - DoClick2    
    else
        print('empty delegate')                                
    end
end

function TestOverride(listener)
    listener:SetOnFinished(TestEventListener.OnClick(DoClick1))
    listener:SetOnFinished(TestEventListener.VoidDelegate(DoClick2))
end

function TestEvent()
    print('this is a event')
end

function AddEvent(listener)
    listener.onClickEvent = listener.onClickEvent + TestEvent
end

function RemoveEvent(listener)
    listener.onClickEvent = listener.onClickEvent - TestEvent
end

local t = {name = 'byself'}

function t:TestSelffunc()
    print('callback with self: '..self.name)
end       

function AddSelfClick(listener)
    if listener.onClick then
        listener.onClick = listener.onClick + TestEventListener.OnClick(t.TestSelffunc, t)
    else
        listener.onClick = TestEventListener.OnClick(t.TestSelffunc, t)
    end   
end     

function RemoveSelfClick(listener)
    if listener.onClick then
        listener.onClick = listener.onClick - TestEventListener.OnClick(t.TestSelffunc, t)
    else
        print('empty delegate')
    end   
end

然后C#层获取这些需要测试的函数,逐一进行测试:

SetClick1 = state.GetFunction("SetClick1");
AddClick1 = state.GetFunction("AddClick1");
AddClick2 = state.GetFunction("AddClick2");
RemoveClick1 = state.GetFunction("RemoveClick1");
RemoveClick2 = state.GetFunction("RemoveClick2");
TestOverride = state.GetFunction("TestOverride");
AddEvent = state.GetFunction("AddEvent");
RemoveEvent = state.GetFunction("RemoveEvent");

AddSelfClick = state.GetFunction("AddSelfClick");
RemoveSelfClick = state.GetFunction("RemoveSelfClick");

if (GUI.Button(new Rect(10, 10, 120, 40), " = OnClick1"))
{
    CallLuaFunction(SetClick1);
}
else if (GUI.Button(new Rect(10, 60, 120, 40), " + Click1"))
{
    CallLuaFunction(AddClick1);
}
else if (GUI.Button(new Rect(10, 110, 120, 40), " + Click2"))
{
    CallLuaFunction(AddClick2);
}
else if (GUI.Button(new Rect(10, 160, 120, 40), " - Click1"))
{
    CallLuaFunction(RemoveClick1);
}
else if (GUI.Button(new Rect(10, 210, 120, 40), " - Click2"))
{
    CallLuaFunction(RemoveClick2);
}
else if (GUI.Button(new Rect(10, 260, 120, 40), "+ Click1 in C#"))
{
    tips = "";
    LuaFunction func = state.GetFunction("DoClick1");
    TestEventListener.OnClick onClick = (TestEventListener.OnClick)DelegateTraits<TestEventListener.OnClick>.Create(func);
    listener.onClick += onClick;
}        
else if (GUI.Button(new Rect(10, 310, 120, 40), " - Click1 in C#"))
{
    tips = "";
    LuaFunction func = state.GetFunction("DoClick1");
    listener.onClick = (TestEventListener.OnClick)DelegateFactory.RemoveDelegate(listener.onClick, func);
    func.Dispose();
    func = null;
}
else if (GUI.Button(new Rect(10, 360, 120, 40), "OnClick"))
{
    if (listener.onClick != null)
    {
        listener.onClick(gameObject);
    }
    else
    {
        Debug.Log("empty delegate!!");
    }
}
else if (GUI.Button(new Rect(10, 410, 120, 40), "Override"))
{
    CallLuaFunction(TestOverride);
}
else if (GUI.Button(new Rect(10, 460, 120, 40), "Force GC"))
{
    state.LuaGC(LuaGCOptions.LUA_GCCOLLECT, 0);
    GC.Collect();
}
else if (GUI.Button(new Rect(10, 510, 120, 40), "event +"))
{
    CallLuaFunction(AddEvent);
}
else if (GUI.Button(new Rect(10, 560, 120, 40), "event -"))
{
    CallLuaFunction(RemoveEvent);
}
else if (GUI.Button(new Rect(10, 610, 120, 40), "event call"))
{
    listener.OnClickEvent(gameObject);
}
else if (GUI.Button(new Rect(200, 10, 120, 40), "+self call"))
{
    CallLuaFunction(AddSelfClick);
}
else if (GUI.Button(new Rect(200, 60, 120, 40), "-self call"))
{
    CallLuaFunction(RemoveSelfClick);
}

测试代码很长,这里我们拆解拆解,先看看SetClick1的调用,它接受一个listener的参数,这个listener是从C#层传入的,它是TestEventListener这个类的对象:

public sealed class TestEventListener : MonoBehaviour
{
    public delegate void VoidDelegate(GameObject go);
    public delegate void OnClick(GameObject go);    
    public OnClick onClick = delegate { };

    public event OnClick onClickEvent = delegate { };

    public Func<bool> TestFunc = null;

    public void SetOnFinished(OnClick click)
    {
        Debugger.Log("SetOnFinished OnClick");
    }
    
    public void SetOnFinished(VoidDelegate click)
    {
        Debugger.Log("SetOnFinished VoidDelegate");
    }

    [NoToLuaAttribute]
    public void OnClickEvent(GameObject go)
    {
        onClickEvent(go);
    }
}

函数SetClick1长这样:

function SetClick1(listener)
    if listener.onClick then
        listener.onClick:Destroy()
    end

    listener.onClick = DoClick1         
end

由于TestEventListener类构造时会将onClick初始化为一个空的委托,因此这里会先走到Destroy上:

static int Destroy(IntPtr L)
{
    Delegate arg0 = (Delegate)ToLua.CheckObject<Delegate>(L, 1);
    Delegate[] ds = arg0.GetInvocationList();

    for (int i = 0; i < ds.Length; i++)
    {
        LuaDelegate ld = ds[i].Target as LuaDelegate;

        if (ld != null)
        {                
            ld.Dispose();                
        }
    }

    return 0;
}

函数会先将栈上的对象转换为Delegate,获取它绑定的所有函数,通过Target属性可以获得绑定函数所属的实例对象。如果是在lua层绑定的,tolua都会使用一个LuaDelegate包装类的对象来完成真正绑定。所以这里的Destroy只会把lua层绑定到委托的函数进行清理。显然这里默认的空函数是不会清理掉的。

回到lua代码,第6行会触发C#层的set_onClick函数:

static int set_onClick(IntPtr L)
{
    object o = null;

    try
    {
        o = ToLua.ToObject(L, 1);
        TestEventListener obj = (TestEventListener)o;
        TestEventListener.OnClick arg0 = (TestEventListener.OnClick)ToLua.CheckDelegate<TestEventListener.OnClick>(L, 2);
        obj.onClick = arg0;
        return 0;
    }
    catch(Exception e)
    {
        return LuaDLL.toluaL_exception(L, e, o, "attempt to index onClick on a nil value");
    }
}

函数最关键的地方在第9行,我们来看看lua函数是如何转换为TestEventListener.OnClick类型的委托的:

public static Delegate CheckDelegate<T>(IntPtr L, int stackPos)
{
    LuaTypes luatype = LuaDLL.lua_type(L, stackPos);

    switch (luatype)
    {
        case LuaTypes.LUA_TFUNCTION:
            LuaFunction func = ToLua.ToLuaFunction(L, stackPos);
            return DelegateTraits<T>.Create(func);
            ...
    }
}

第8行首先把lua函数转换为LuaFunction包装类的对象。第9行的DelegateTraits和我们之前提到的TypeTraits类似,会根据不同的类型,调用不同的Create函数来创建不同类型的委托:

static public Delegate Create(LuaFunction func)
{
    if (func != null)
    {
        LuaState state = func.GetLuaState();
        LuaDelegate target = state.GetLuaDelegate(func);

        if (target != null)
        {
            return Delegate.CreateDelegate(typeof(T), target, target.method);
        }
        else
        {
            Delegate d = _Create(func, null, false);
            target = d.Target as LuaDelegate;
            state.AddLuaDelegate(target, func);
            return d;
        }
    }

    return _Create(null, null, false);            
}

LuaFunction类似,tolua使用LuaDelegate来表示C#层当前引用的lua层委托,第6行的GetLuaDelegate就是从C#的delegateMap缓存中查找LuaFunction对应的LuaDelegate是否存在,如果存在就直接取出,调用C#的CreateDelegate函数创建委托,注意这里的target设置成了当前的LuaDelegate对象,这样Destroy的时候就不怕找不到它了。另外一点值得注意的是,它和LuaFunction不同,LuaFunction对于同一个lua函数,只会在C#层中保存一份,然后C#层可以直接通过LuaFunction.PCall调用lua函数;而LuaDelegate对于同一个lua函数,虽然也只会在C#层保存一份,但是转换成C#委托进行调用时,每次都会创建一个新的委托。也就是说,我们在lua层对C#委托赋值时,最好只在初始化的时候赋值一次。

如果LuaDelegate不存在,则会调用对应类型的Create函数创建委托。例如这里的TestEventListener.OnClick

class TestEventListener_OnClick_Event : LuaDelegate
{
    public TestEventListener_OnClick_Event(LuaFunction func) : base(func) { }
    public TestEventListener_OnClick_Event(LuaFunction func, LuaTable self) : base(func, self) { }


    public void Call(UnityEngine.GameObject param0)
    {
        func.BeginPCall();
        func.Push(param0);
        func.PCall();
        func.EndPCall();
    }

    public void CallWithSelf(UnityEngine.GameObject param0)
    {
        func.BeginPCall();
        func.Push(self);
        func.Push(param0);
        func.PCall();
        func.EndPCall();
    }

}

public static TestEventListener.OnClick TestEventListener_OnClick(LuaFunction func, LuaTable self, bool flag)
{
    if (func == null)
    {
        TestEventListener.OnClick fn = delegate(UnityEngine.GameObject param0) { };
        return fn;
    }

    if(!flag)
    {
        TestEventListener_OnClick_Event target = new TestEventListener_OnClick_Event(func);
        TestEventListener.OnClick d = target.Call;
        target.method = d.Method;
        return d;
    }
    else
    {
        TestEventListener_OnClick_Event target = new TestEventListener_OnClick_Event(func, self);
        TestEventListener.OnClick d = target.CallWithSelf;
        target.method = d.Method;
        return d;
    }
}

TestEventListener_OnClick就是创建委托的函数,它有个标志位flag表示是否带有self参数,这里由于直接set赋值是没有的,所以flag为false。TestEventListener_OnClick_Event就是包装类,它继承自LuaDelegate,拥有CallCallWithSelf两个方法,可以看出带有self的方法,就是把构造时传入的self,当作函数的一个参数压入lua栈,给lua层调用,这就支持了lua层带有self语法糖的函数,这样的函数也可以绑定到C#的委托上。

成功创建委托之后,需要把委托记录到C#缓存中,避免对同一个lua函数多次创建LuaDelegate包装类对象。此时,如果我们在C#层触发listener.onClick,就会调用到TestEventListener_OnClick_Event.Call,进而调用到lua层的DoClick1函数,输出如下:

tolua源码分析(六)1

接下来我们来看下委托的+=机制在lua层的实现。对应到例子里就是AddClick1AddClick2这两个函数:

function AddClick1(listener)       
    if listener.onClick then
        listener.onClick = listener.onClick + DoClick1                                                    
    else
        listener.onClick = DoClick1                      
    end                
end

function AddClick2(listener)
    if listener.onClick then
        listener.onClick = listener.onClick + DoClick2                      
    else
        listener.onClick = DoClick2
    end                
end

tolua对+操作符进行了重载,即在metatable中设置了__add元方法,会调用到C#的op_Addition

static int op_Addition(IntPtr L)
{
    try
    {                        
        LuaTypes type = LuaDLL.lua_type(L, 1);

        switch (type)
        {
            case LuaTypes.LUA_TUSERDATA:
                Delegate a0 = ToLua.ToObject(L, 1) as Delegate;
                Delegate a1 = ToLua.CheckDelegate(a0.GetType(), L, 2);
                Delegate ret = Delegate.Combine(a0, a1);
                ToLua.Push(L, ret);
                return 1;
                ...
        }
    }
    catch (Exception e)
    {
        return LuaDLL.toluaL_exception(L, e);
    }
}

+左边的listener.onClick是个C#对象,显然是个userdata,根据其index,从C#缓存中查找对应的Delegate,右侧为lua函数,同样需要将其转换为Delegate,过程与前面类似,这里就不重复了。得到两个Delegate之后,调用C#的Combine函数合成最终的委托,压入lua层。调用AddClick1AddClick2后再触发触发listener.onClick的结果输出如下:

tolua源码分析(六)2

类似地,tolua对-操作符也进行了重载,会调用到C#的op_Subtraction,它的逻辑就是找到持有相同LuaFunctionLuaDelegate,然后从委托中移除:

static int op_Subtraction(IntPtr L)
{
    try
    {            
        Delegate arg0 = (Delegate)ToLua.CheckObject<Delegate>(L, 1);
        LuaTypes type = LuaDLL.lua_type(L, 2);

        if (type == LuaTypes.LUA_TFUNCTION)
        {
            LuaState state = LuaState.Get(L);
            LuaFunction func = ToLua.ToLuaFunction(L, 2);
            Delegate[] ds = arg0.GetInvocationList();

            for (int i = 0; i < ds.Length; i++)
            {
                LuaDelegate ld = ds[i].Target as LuaDelegate;

                if (ld != null && ld.func == func && ld.self == null)
                {
                    arg0 = Delegate.Remove(arg0, ds[i]);
                    state.DelayDispose(ld.func);
                    break;
                }
            }

            func.Dispose();
            ToLua.Push(L, arg0);
            return 1;
        }
        else
        {
            ...
        }
    }
    catch (Exception e)
    {
        return LuaDLL.toluaL_exception(L, e);
    }
}

这里需要特别注意的是DelayDispose这个函数。tolua中C#引用的lua table/function对象均继承自LuaBaseRef这个类,它通过引用计数的形式来判断lua对象是否仍被C#引用。如果引用计数为0,说明C#不再引用,则包装类对象可被清理等待C# gc回收,同时lua层缓存在注册表中的对象也可被清理,即lua层也可gc掉该对象、而对于这里的委托来说,情况稍微复杂一些,由于LuaDelegate对象是被缓存在delegateMap中的,而它本身又引用住了一个LuaFunction对象,所以如果这里直接Dispose掉LuaFunction对象,其引用计数很可能在出函数作用域时变为0,导致LuaFunction里的数据无效,而LuaDelegate对象并不一定会被Dipose掉,这就可能导致一个有效的LuaDelegate对象持有一个无效的LuaFunction对象。所以,为了避免这种情况发生,DelayDispose函数会把LunFunction对象放在一个延迟列表里引用住,避免出作用域被Dispose。

然后我们来看看带有self的lua函数是怎么绑定的:

function AddSelfClick(listener)
    if listener.onClick then
        listener.onClick = listener.onClick + TestEventListener.OnClick(t.TestSelffunc, t)
    else
        listener.onClick = TestEventListener.OnClick(t.TestSelffunc, t)
    end   
end 

可以发现这里没有优雅的写法,是通过传入lua函数和self table,构造出C#对象的方式,来绑定委托的。TestEventListener.OnClick的实现也比较简单,就是调用对应Delegate类型的Create函数:

static int TestEventListener_OnClick(IntPtr L)
{
    try
    {
        int count = LuaDLL.lua_gettop(L);
        LuaFunction func = ToLua.CheckLuaFunction(L, 1);

        if (count == 1)
        {
            Delegate arg1 = DelegateTraits<TestEventListener.OnClick>.Create(func);
            ToLua.Push(L, arg1);
        }
        else
        {
            LuaTable self = ToLua.CheckLuaTable(L, 2);
            Delegate arg1 = DelegateTraits<TestEventListener.OnClick>.Create(func, self);
            ToLua.Push(L, arg1);
        }
        return 1;
    }
    catch(Exception e)
    {
        return LuaDLL.toluaL_exception(L, e);
    }
}

带有self table的Create函数大同小异,在缓存给C#的delegateMap上做了一点特殊处理。delegateMap的key实际是对应到lua层的reference,带table意味着有两个reference,所以这里的key是64位的long型,两个reference各占高低32位,这样就能通过一个key把两个reference都记录下来了。这时的输出结果如下:

tolua源码分析(六)3

最后,我们来看一下event。C#的event规定了只能使用+=/-=操作,不允许在外部使用=修改event。

function AddEvent(listener)
    listener.onClickEvent = listener.onClickEvent + TestEvent
end

那么lua层是如何实现这样的机制呢?首先是get_onClickEvent,它返回的并不是一个委托,而是一个EventObject,注意每次调用该函数时,都会新建一个EventObject对象,它是有gc开销的:

static int get_onClickEvent(IntPtr L)
{
    ToLua.Push(L, new EventObject(typeof(TestEventListener.OnClick)));
    return 1;
}

public class EventObject
{
    [NoToLuaAttribute]
    public EventOp op = EventOp.None;
    [NoToLuaAttribute]
    public Delegate func = null;
    [NoToLuaAttribute]
    public Type type;

    [NoToLuaAttribute]
    public EventObject(Type t)
    {
        type = t;
    }

    public static EventObject operator +(EventObject a, Delegate b)
    {
        a.op = EventOp.Add;
        a.func = b;
        return a;
    }

    public static EventObject operator -(EventObject a, Delegate b)
    {
        a.op = EventOp.Sub;
        a.func = b;
        return a;
    }
}

EventObject类保存了持有的委托,委托的类型,以及当前执行的操作。在对应的op_Addition中,会对这些值进行修改:

static int op_Addition(IntPtr L)
{
    try
    {
        EventObject arg0 = (EventObject)ToLua.CheckObject(L, 1, typeof(EventObject));
        arg0.func = ToLua.CheckDelegate(arg0.type, L, 2);
        arg0.op = EventOp.Add;
        ToLua.Push(L, arg0);
        return 1;
    }
    catch (Exception e)
    {
        return LuaDLL.toluaL_exception(L, e);
    }
}

然后,在set_onClickEvent中,会对lua栈上的变量进行检查,只有可以表示EventObject类的userdata才是合法的,这就阻止了直接将一个lua函数赋值给event。根据EventObject类保存的信息,是+还是-,对真正的event进行操作:

static int set_onClickEvent(IntPtr L)
{
    try
    {
        TestEventListener obj = (TestEventListener)ToLua.CheckObject(L, 1, typeof(TestEventListener));
        EventObject arg0 = null;

        if (LuaDLL.lua_isuserdata(L, 2) != 0)
        {
            arg0 = (EventObject)ToLua.ToObject(L, 2);
        }
        else
        {
            return LuaDLL.luaL_throw(L, "The event 'TestEventListener.onClickEvent' can only appear on the left hand side of += or -= when used outside of the type 'TestEventListener'");
        }

        if (arg0.op == EventOp.Add)
        {
            TestEventListener.OnClick ev = (TestEventListener.OnClick)arg0.func;
            obj.onClickEvent += ev;
        }
        else if (arg0.op == EventOp.Sub)
        {
            TestEventListener.OnClick ev = (TestEventListener.OnClick)arg0.func;
            obj.onClickEvent -= ev;
        }

        return 0;
    }
    catch (Exception e)
    {
        return LuaDLL.toluaL_exception(L, e);
    }
}

运行的结果如下:

tolua源码分析(六)4

总结一下,一个lua函数想要绑定到C#委托时,首先它会被包装为C#LuaFunction类对象,然后被更上层的C#LuaDelegate类对象所引用,最后绑定到指定的委托上。其中,LuaFunction对象和LuaDelegate对象都会被缓存,在使用时注意lua层需要在用完C#委托时及时释放掉,不要一直保留对其的引用,否则可能会出现内存泄漏的问题。绑定到C#事件时,还要再多一步,LuaDelegate会被EventObject类型的对象所引用,它还包含了当前是绑定还是解绑的操作类型,只有该类型的对象才能赋值到C#事件,根据操作类型,绑定lua函数或是解绑lua函数。需要注意的是,每次在lua层获取C#事件时,都会创建一个新的EventObject对象,C#层没有专门针对该类型对象的缓存。总之,在实际项目中,要对委托和事件的使用慎之又慎。

下一节我们将探讨C#中带out参数的函数是如何在lua层使用的。

如果你觉得我的文章有帮助,欢迎关注我的微信公众号 我是真的想做游戏啊

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/575672.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【沐风老师】3DMAX宇宙生成器(一键生成星系)插件使用方法详解

3DMAX宇宙生成器&#xff08;一键生成星系&#xff09;插件 3DMAX宇宙生成器&#xff08;一键生成星系&#xff09;插件&#xff0c;用于模拟星团及星系的运动。可以创建单个集合进行动画计算&#xff0c;也可以输入不同坐标&#xff0c;建立多个集合后统一进行动画计算。 【安…

探索iOS之CoreImage框架

CoreImage提供图像处理、人脸识别、图像增强、图像滤镜、图像转场。它操作的数据来自Core Graphics、Core Video、Image IO&#xff0c;使用CPU或GPU进行渲染。CoreImage对底层实现进行封装&#xff0c;为上层提供简单易用的API。 一、CoreImage框架 CoreImage框架分为&#…

计算机电脑中了勒索病毒,如何提高windows系统预防faust勒索病毒的能力

当今社会互联网技术迅猛发展&#xff0c;但随之而来的网络安全问题开始引起人们的重视&#xff0c;其中,勒索软件是当前网络安全领域中最具破坏性和危害性的恶意软件之一&#xff0c;而Faust勒索软件就是其中一种。 Faust勒索病毒是一种很可怕的恶意软件&#xff0c;能够破坏并…

计算机网络 - 网络层的数据平面

Overview 首先Network Layer负责的是host to host的传输, 然后可以分为两个平面, 控制平面以及数据平面. 数据平面: 负责forward datagrams from input to output links 决定路由器从input到output 转发功能: 基于目标地址 转发表 SDN方式基于多个字段流表 控制平面: 调…

第10届蓝桥杯Scratch省赛真题集锦

编程题 第 1 题 问答题 击鼓游戏 题目说明 准备工作: 将复台背景设置为“spotight-stage”&#xff0c;添加一个“Belerina"角色、两个“Drum1"角色和两个“Drum2”角色&#xff0c;并按照图 7-1 的位置摆放。角色“Beleina"的造型和颜色的设置须如图 7-1所示&a…

美女放电还是整形-从《人月神话》误译谈状态机图

DDD领域驱动设计批评文集 欢迎加入“软件方法建模师”群 《软件方法》各章合集 在重审《人月神话》中译本的过程中&#xff0c;发现一处较严重的误译。 原文是&#xff1a; the first milepost is not reached until two months have elapsed. 原译为&#xff1a; 两个月…

Java中常量基础知识

1 问题 什么是字面常量&#xff1f; 2 方法 拿第一行输出语句来说&#xff0c;这行代码输出了 “hello” 这个字符串&#xff0c;无论何时何地运行程序&#xff0c;输出的这个字符串都不会变&#xff0c;这就是字面常量。 定义&#xff1a;常量即程序运行期间&#xff0c;固定不…

chatgpt网站优选集合

目录 chatGpt 国内镜像网站集合 chatgpt接入微信 chatgpt接入谷歌浏览器 chatgptAI绘画 最后 chatGpt 这个说实话&#xff0c;不用我介绍大家也都懂。不如玩一个有意思的&#xff0c;这里我让chatGpt自己介绍一下他自己&#xff1a; 嗨&#xff0c;大家好&#xff0c;我是…

C++ [STL之vector模拟实现]

本文已收录至《C语言和高级数据结构》专栏&#xff01; 作者&#xff1a;ARMCSKGT STL之vector模拟实现 前言正文空间结构默认成员函数构造函数拷贝构造函数赋值重载析构函数关于数据拷贝问题 迭代器容量操作查询容量容量操作 数据访问下标访问头尾数据访问 数据增删尾插尾删重…

学懂缓存雪崩,缓存击穿,缓存穿透仅需一篇,基于Redis讲解

在了解缓存雪崩、击穿、穿透这三个问题前&#xff0c;我们需要知道为什么我们需要缓存。在了解这三个问题后&#xff0c;我们也必须知道使用Redis时&#xff0c;如何解决这些问题。 所以我将按照"为什么我们需要缓存"、"什么是缓存雪崩、击穿、穿透"、&qu…

Linux学习笔记 --- Linux基础命令Part1

一. Linux的目录结构 目标&#xff1a;1. 掌握Linux系统的目录结构 2. 掌握Linux系统的路径表达形式 1.1 Linux系统的目录结构 我们知道&#xff0c;在操作系统中&#xff0c;文件系统的目录结构一般都是树形结构。Linux的目录结构是一个树型结构Windows 系统可以拥有多…

如何使用wireShark抓取还原文件

简介 WireShark的追踪流功能可以帮我们抓取从网络上下载的各种文件&#xff0c;接下来就演示下如何抓取并且进行还原。 使用Nginx搭建文件存储服务器 只要是通过http网站下载的包&#xff0c;都可以通过追踪流工具进行抓取。这里为了演示&#xff0c;临时搭建一个Nginx文件存…

Yolov8涨点技巧:MobileViTAttention助力小目标检测,涨点显著,MobileViT移动端轻量通用视觉transformer

在​crack缺陷检测项目原始0.739提升至 0.772 ,涨点明显,博主多个数据集亲测有效,实现暴力涨点; 现有博客都是将MobileViT作为backbone引入Yolov5,因此存在的问题点是训练显存要求巨大,本文引入自注意力的Vision Transformer(ViTs):MobileViTAttention 1. MobileViT介绍…

English Learning - L3 作业打卡 Lesson3 Day18 2023.5.22 周一

English Learning - L3 作业打卡 Lesson3 Day18 2023.5.22 周一 引言&#x1f349;句1: People said my mother was “a good egg”.成分划分弱读连读语调 &#x1f349;句2: She would always help anyone in need.成分划分连读语调 &#x1f349;句3: We never had to “walk…

1、MQTT协议入门以及mosquitto示例

1.什么是MQTT MQTT&#xff08;Message Queuing Telemetry Transport&#xff0c;消息队列遥测传输协议&#xff09;&#xff0c;是一种基于发布/订阅&#xff08;publish/subscribe&#xff09;模式的"轻量级"通讯协议&#xff0c;该协议构建于TCP/IP协议上&#xf…

一款好看的markdown编辑器:md-editor-v3

文章目录 md-eidtor-v3地址其它markdown编辑器&#x1f384; md-editor-v3⭐️ 功能一览&#x1f4e6; 安装&#x1f4a1; 用法✍&#x1f3fb; 编辑器模式&#x1f4d6; 仅预览模式 &#x1f5fa; 预览图&#x1f381; Apis&#x1f516; MdPreivew Props&#x1f529; MdEdit…

c++基础概念,const与指针、引用的关系,auto,decltype关键字能干啥总得了解吧。总得按照需求自定义创建实体类,自己编写头文件吧

const限定符 有时我们希望定义这样一种变量&#xff0c;它的值不能被改变。例如&#xff0c;用一个变量来表示缓冲区的大小。使用变量的好处是当我们觉得缓冲区大小不再合适时&#xff0c;很容易对其进行调整。另一方面&#xff0c;也应随时警惕防止程序一不小心改变了这个值。…

邂逅Webpack和打包过程

1 认识webpack工具 2 webpack基本打包 3 webpack配置文件 4 编写和打包CSS文件 5 编写和打包LESS文件 6 postcss工具处理CSS 从脚手架打包成普通的htmlcssjs文件 内置模块path&#xff1a; 拼接路径 const path require("path")const filepath "C://abc/…

Systrace系列9 —— MainThread 和 RenderThread 解读

本文是介绍 Android App 中的 MainThread 和 RenderThread,也就是大家熟悉的主线程和渲染线程。文章会从 Systrace 的角度来看 MainThread 和 RenderThread 的工作流程,以及涉及到的相关知识:卡顿、软件渲染、掉帧计算等。 这里以滑动列表为例 ,我们截取主线程和渲染线程一…

InnoDB 在可重复读 RR 隔离级别下,能解决幻读问题吗?

InnoDB 在可重复读 RR 隔离级别下&#xff0c;能解决幻读问题吗&#xff1f; MySQL 在「可重复读」隔离级别下&#xff0c;可以很大程度上避免幻读现象的发生&#xff08;注意是很大程度避免&#xff0c;并不是彻底避免&#xff09;&#xff0c;所以 MySQL 并不会使用「串行化」…