xlua源码分析(三)C#访问lua的映射

news2024/9/28 7:23:19

xlua源码分析(三)C#访问lua的映射

上一节我们主要分析了lua call C#的无wrap实现。同时我们在第一节里提到过,C#使用LuaTable类持有lua层的table,以及使用Action委托持有lua层的function。而在xlua的官方文档中,推荐使用interface和delegate访问lua层数据结构:

映射到一个interface

这种方式依赖于生成代码(如果没生成代码会抛InvalidCastException异常),代码生成器会生成这个interface的实例,如果get一个属性,生成代码会get对应的table字段,如果set属性也会设置对应的字段。甚至可以通过interface的方法访问lua的函数。

映射到delegate

这种是建议的方式,性能好很多,而且类型安全。缺点是要生成代码(如果没生成代码会抛InvalidCastException异常)。

delegate要怎样声明呢? 对于function的每个参数就声明一个输入类型的参数。 多返回值要怎么处理?从左往右映射到c#的输出参数,输出参数包括返回值,out参数,ref参数。

参数、返回值类型支持哪些呢?都支持,各种复杂类型,out,ref修饰的,甚至可以返回另外一个delegate。

delegate的使用就更简单了,直接像个函数那样用就可以了。

那么这一节我们就对照着Examples 04_LuaObjectOrented,来看一下如何把包含任意数据的lua table和包含任意参数的lua function映射到C#,让C#可以直接访问。

首先看一下例子中用到的lua代码:

local calc_mt = {
    __index = {
        Add = function(self, a, b)
            return (a + b) * self.Mult
        end,
        
        get_Item = function(self, index)
            return self.list[index + 1]
        end,

        set_Item = function(self, index, value)
            self.list[index + 1] = value
            self:notify({name = index, value = value})
        end,
        
        add_PropertyChanged = function(self, delegate)
            if self.notifylist == nil then
                self.notifylist = {}
            end
            table.insert(self.notifylist, delegate)
            print('add',delegate)
        end,
                                
        remove_PropertyChanged = function(self, delegate)
            for i=1, #self.notifylist do
                if CS.System.Object.Equals(self.notifylist[i], delegate) then
                    table.remove(self.notifylist, i)
                    break
                end
            end
            print('remove', delegate)
        end,

        notify = function(self, evt)
            if self.notifylist ~= nil then
                for i=1, #self.notifylist do
                    self.notifylist[i](self, evt)
                end
            end	
        end,
    }
}

Calc = {
    New = function (mult, ...)
        print(...)
        return setmetatable({Mult = mult, list = {'aaaa','bbbb','cccc'}}, calc_mt)
    end
}

这个例子很简单,就是定义了一个Calc.New的函数,这个函数会使用传入的参数构建一个新的table,并设置calc_mt作为它的metatable。calc_mt的__index表中定义了若干供C#访问的函数,如Addget_Itemset_Itemadd_PropertyChangedremove_PropertyChanged

回到C#,C#层如果想要访问lua层的Calc.New,就需要定义一个和该函数匹配的委托。这个委托定义如下:

[CSharpCallLua]
public delegate ICalc CalcNew(int mult, params string[] args);

委托有一个int类型的参数mult和不定数量的string类型参数args,int和string类型都可以很容易地从C#类型转换到对应的lua类型。再看返回值,这里的返回类型是一个ICalc的interface,它其实映射就是lua层的table,也就是Calc.New所返回的那个table。为了让xlua识别CalcNew这个委托类型是用来映射lua函数的,也就是要使用这个委托调用lua层函数,需要给CalcNew类型打上CSharpCallLua的标签,这样xlua就会生成代码来完成这一工作。

映射lua table的ICalc定义如下:

[CSharpCallLua]
public interface ICalc
{
    event EventHandler<PropertyChangedEventArgs> PropertyChanged;

    int Add(int a, int b);
    int Mult { get; set; }

    object this[int index] { get; set; }
}

接口类中包含了一个PropertyChanged的event,一个Add方法,一个Multi属性,还实现了下标操作符。那么想必大家都能猜出来,这里就是分别对应了lua层calc_mt的__index表中定义的若干函数。同样地,我们也需要为这个interface打上[CSharpCallLua]标签,这样xlua就会生成一个具体实现该接口的类。

在理解映射思路之后,我们再看下测试代码:

void Test(LuaEnv luaenv)
{
    luaenv.DoString(script);
    CalcNew calc_new = luaenv.Global.GetInPath<CalcNew>("Calc.New");
    ICalc calc = calc_new(10, "hi", "john"); //constructor
    Debug.Log("sum(*10) =" + calc.Add(1, 2));
    calc.Mult = 100;
    Debug.Log("sum(*100)=" + calc.Add(1, 2));

    Debug.Log("list[0]=" + calc[0]);
    Debug.Log("list[1]=" + calc[1]);

    calc.PropertyChanged += Notify;
    calc[1] = "dddd";
    Debug.Log("list[1]=" + calc[1]);

    calc.PropertyChanged -= Notify;

    calc[1] = "eeee";
    Debug.Log("list[1]=" + calc[1]);
}

void Notify(object sender, PropertyChangedEventArgs e)
{
    Debug.Log(string.Format("{0} has property changed {1}={2}", sender, e.name, e.value));
}

运行之后输出结果如下:

xlua源码分析(三)1

可以看到,我们通过映射的方式,访问到了lua的函数和table,而且很重要的一点是,测试代码中C#和lua实现了解耦,这种做法也是xlua的官方文档中所推荐的:

使用建议

  1. 访问lua全局数据,特别是table以及function,代价比较大,建议尽量少做,比如在初始化时把要调用的lua function获取一次(映射到delegate)后,保存下来,后续直接调用该delegate即可。table也类似。
  2. 如果lua侧的实现的部分都以delegate和interface的方式提供,使用方可以完全和xLua解耦:由一个专门的模块负责xlua的初始化以及delegate、interface的映射,然后把这些delegate和interface设置到要用到它们的地方。

那么现在,我们开始,跟着测试代码,一步步地研究背后的实现吧。

第一步,就是调用了GetInPath,通过变量的名称获取到lua函数,再将其转换为CalcNew委托类型:

public T GetInPath<T>(string path)
{
#if THREAD_SAFE || HOTFIX_ENABLE
    lock (luaEnv.luaEnvLock)
    {
#endif
        var L = luaEnv.L;
        var translator = luaEnv.translator;
        int oldTop = LuaAPI.lua_gettop(L);
        LuaAPI.lua_getref(L, luaReference);
        if (0 != LuaAPI.xlua_pgettable_bypath(L, -1, path))
        {
            luaEnv.ThrowExceptionFromError(oldTop);
        }
        LuaTypes lua_type = LuaAPI.lua_type(L, -1);
        if (lua_type == LuaTypes.LUA_TNIL && typeof(T).IsValueType())
        {
            throw new InvalidCastException("can not assign nil to " + typeof(T).GetFriendlyName());
        }

        T value;
        try
        {
            translator.Get(L, -1, out value);
        }
        catch (Exception e)
        {
            throw e;
        }
        finally
        {
            LuaAPI.lua_settop(L, oldTop);
        }
        return value;
#if THREAD_SAFE || HOTFIX_ENABLE
    }
#endif
}

重点需要关注的其实就是这句translator.Get(L, -1, out value);,它负责对lua栈上的函数进行类型转换。这个委托类型并不是实现注册好的类型,那么就会走到通用的GetObject函数:

public void Get<T>(RealStatePtr L, int index, out T v)
{
    Func<RealStatePtr, int, T> get_func;
    if (tryGetGetFuncByType(typeof(T), out get_func))
    {
        v = get_func(L, index);
    }
    else
    {
        v = (T)GetObject(L, index, typeof(T));
    }
}

这个GetObject函数我们在前面的章节中也分析过,对于不是userdata的lua对象,它会寻找一个caster函数进行转换,如果找不到,则会通过一系列规则生成一个caster:

public ObjectCast GetCaster(Type type)
{
    if (type.IsByRef) type = type.GetElementType();

    Type underlyingType = Nullable.GetUnderlyingType(type);
    if (underlyingType != null)
    {
        return genNullableCaster(GetCaster(underlyingType)); 
    }
    ObjectCast oc;
    if (!castersMap.TryGetValue(type, out oc))
    {
        oc = genCaster(type);
        castersMap.Add(type, oc);
    }
    return oc;
}

这里的委托类型是我们自定义的,默认的castersMap中显然不包含,那么xlua就会为我们生成一个:

ObjectCast fixTypeGetter = (RealStatePtr L, int idx, object target) =>
{
    if (LuaAPI.lua_type(L, idx) == LuaTypes.LUA_TUSERDATA)
    {
        object obj = translator.SafeGetCSObj(L, idx);
        return (obj != null && type.IsAssignableFrom(obj.GetType())) ? obj : null;
    }
    return null;
}; 

if (typeof(Delegate).IsAssignableFrom(type))
{
    return (RealStatePtr L, int idx, object target) =>
    {
        object obj = fixTypeGetter(L, idx, target);
        if (obj != null) return obj;

        if (!LuaAPI.lua_isfunction(L, idx))
        {
            return null;
        }

        return translator.CreateDelegateBridge(L, type, idx);
    };
}

这里的关键也是在translator.CreateDelegateBridge这句,这个函数之前我们也分析过,它负责生成一个DelegateBridge对象。这个对象就是指代lua函数用的,它自身可以与多个C#的委托绑定。

bridge = new DelegateBridge(reference, luaEnv);
try {
    var ret = getDelegate(bridge, delegateType);
    bridge.AddDelegate(delegateType, ret);
    delegate_bridges[reference] = new WeakReference(bridge);
    return ret;
}
catch(Exception e)
{
    bridge.Dispose();
    throw e;
}

getDelegate这个函数,会根据传入的delegateType,调用DelegateBridgeBase.GetDelegateByType生成对应类型的Delegate对象,它是个virtual方法,我们在生成代码之后,就会产生继承自它的DelegateBridge.GetDelegateByTypeoverride方法,这段生成代码位于DelegatesGenBridge.cs这个文件里:

public partial class DelegateBridge : DelegateBridgeBase
{
    public override Delegate GetDelegateByType(Type type)
    {
        if (type == typeof(System.Action))
        {
            return new System.Action(__Gen_Delegate_Imp0);
        }

        if (type == typeof(UnityEngine.Events.UnityAction))
        {
            return new UnityEngine.Events.UnityAction(__Gen_Delegate_Imp0);
        }

        if (type == typeof(System.Func<double, double, double>))
        {
            return new System.Func<double, double, double>(__Gen_Delegate_Imp1);
        }

        if (type == typeof(System.Action<string>))
        {
            return new System.Action<string>(__Gen_Delegate_Imp2);
        }

        if (type == typeof(System.Action<double>))
        {
            return new System.Action<double>(__Gen_Delegate_Imp3);
        }

        if (type == typeof(XLuaTest.IntParam))
        {
            return new XLuaTest.IntParam(__Gen_Delegate_Imp4);
        }

        if (type == typeof(XLuaTest.Vector3Param))
        {
            return new XLuaTest.Vector3Param(__Gen_Delegate_Imp5);
        }

        if (type == typeof(XLuaTest.CustomValueTypeParam))
        {
            return new XLuaTest.CustomValueTypeParam(__Gen_Delegate_Imp6);
        }

        if (type == typeof(XLuaTest.EnumParam))
        {
            return new XLuaTest.EnumParam(__Gen_Delegate_Imp7);
        }

        if (type == typeof(XLuaTest.DecimalParam))
        {
            return new XLuaTest.DecimalParam(__Gen_Delegate_Imp8);
        }

        if (type == typeof(XLuaTest.ArrayAccess))
        {
            return new XLuaTest.ArrayAccess(__Gen_Delegate_Imp9);
        }

        if (type == typeof(System.Action<bool>))
        {
            return new System.Action<bool>(__Gen_Delegate_Imp10);
        }

        if (type == typeof(Tutorial.CSCallLua.FDelegate))
        {
            return new Tutorial.CSCallLua.FDelegate(__Gen_Delegate_Imp11);
        }

        if (type == typeof(Tutorial.CSCallLua.GetE))
        {
            return new Tutorial.CSCallLua.GetE(__Gen_Delegate_Imp12);
        }

        if (type == typeof(XLuaTest.InvokeLua.CalcNew))
        {
            return new XLuaTest.InvokeLua.CalcNew(__Gen_Delegate_Imp13);
        }

        return null;
    }
}

得到Delegate之后,这里会将其进行缓存,这样下次遇到相同类型直接取出该委托即可。DelegateBridgeBase类缓存Delegate的数据结构比较有意思,它有一对firstKey和firstValue,然后一个Dictionary<Type, Delegate>的字典所组成,缓存时会优先将数据保存到firstKey和firstValue上,这样取出的时候就无需对字典进行查找,查找效率更高。

public bool TryGetDelegate(Type key, out Delegate value)
{
    if(key == firstKey)
    {
        value = firstValue;
        return true;
    }
    if (bindTo != null)
    {
        return bindTo.TryGetValue(key, out value);
    }
    value = null;
    return false;
}

public void AddDelegate(Type key, Delegate value)
{
    if (key == firstKey)
    {
        throw new ArgumentException("An element with the same key already exists in the dictionary.");
    }

    if (firstKey == null && bindTo == null) // nothing 
    {
        firstKey = key;
        firstValue = value;
    }
    else if (firstKey != null && bindTo == null) // one key existed
    {
        bindTo = new Dictionary<Type, Delegate>();
        bindTo.Add(firstKey, firstValue);
        firstKey = null;
        firstValue = null;
        bindTo.Add(key, value);
    }
    else
    {
        bindTo.Add(key, value);
    }
}

就这样,这个新生成的委托经过辗转终于返回到了测试代码,也就是calc_new对象,那么我们就可以直接通过委托的方式调用它,此时就会触发生成的__Gen_Delegate_Imp13函数了,我们来看看生成的代码长什么样:

public XLuaTest.InvokeLua.ICalc __Gen_Delegate_Imp13(int p0, string[] p1)
{
#if THREAD_SAFE || HOTFIX_ENABLE
    lock (luaEnv.luaEnvLock)
    {
#endif
        RealStatePtr L = luaEnv.rawL;
        int errFunc = LuaAPI.pcall_prepare(L, errorFuncRef, luaReference);
        ObjectTranslator translator = luaEnv.translator;
        LuaAPI.xlua_pushinteger(L, p0);
        if (p1 != null)  { for (int __gen_i = 0; __gen_i < p1.Length; ++__gen_i) LuaAPI.lua_pushstring(L, p1[__gen_i]); };
        
        PCall(L, 1 + (p1 == null ? 0 : p1.Length), 1, errFunc);
        
        
        XLuaTest.InvokeLua.ICalc __gen_ret = (XLuaTest.InvokeLua.ICalc)translator.GetObject(L, errFunc + 1, typeof(XLuaTest.InvokeLua.ICalc));
        LuaAPI.lua_settop(L, errFunc - 1);
        return  __gen_ret;
#if THREAD_SAFE || HOTFIX_ENABLE
    }
#endif
}

代码逻辑很简单,就是准备调用环境,然后把C#的参数push到lua层,然后pcall调用,然后从lua栈中取出返回的结果,由于lua是弱类型的,无法事先知道返回值的类型,所以这里只能使用通用的GetObject函数对lua的返回值进行类型转换。

同样,ICalc类型是我们自定义的,默认的castersMap是不包含的,也需要生成一个caster:

return (RealStatePtr L, int idx, object target) =>
{
    object obj = fixTypeGetter(L, idx, target);
    if (obj != null) return obj;

    if (!LuaAPI.lua_istable(L, idx))
    {
        return null;
    }
    return translator.CreateInterfaceBridge(L, type, idx);
};

那么,这里的关键就是在translator.CreateInterfaceBridge上了,与委托非常类似,这里会根据interface的类型,寻找负责生成interface对象的函数:

public object CreateInterfaceBridge(RealStatePtr L, Type interfaceType, int idx)
{
    Func<int, LuaEnv, LuaBase> creator;

    if (!interfaceBridgeCreators.TryGetValue(interfaceType, out creator))
    {
#if (UNITY_EDITOR || XLUA_GENERAL) && !NET_STANDARD_2_0
        var bridgeType = ce.EmitInterfaceImpl(interfaceType);
        creator = (int reference, LuaEnv luaenv) =>
        {
            return Activator.CreateInstance(bridgeType, new object[] { reference, luaEnv }) as LuaBase;
        };
        interfaceBridgeCreators.Add(interfaceType, creator);
#else
        throw new InvalidCastException("This type must add to CSharpCallLua: " + interfaceType);
#endif
    }
    LuaAPI.lua_pushvalue(L, idx);
    return creator(LuaAPI.luaL_ref(L), luaEnv);
}

往interfaceBridgeCreators注册creator的逻辑就是在生成代码中完成的,位于XLuaGenAutoRegister.cs中:

static void Init(LuaEnv luaenv, ObjectTranslator translator)
{
    
    wrapInit0(luaenv, translator);
    
    
    translator.AddInterfaceBridgeCreator(typeof(System.Collections.IEnumerator), SystemCollectionsIEnumeratorBridge.__Create);
    
    translator.AddInterfaceBridgeCreator(typeof(XLuaTest.IExchanger), XLuaTestIExchangerBridge.__Create);
    
    translator.AddInterfaceBridgeCreator(typeof(Tutorial.CSCallLua.ItfD), TutorialCSCallLuaItfDBridge.__Create);
    
    translator.AddInterfaceBridgeCreator(typeof(XLuaTest.InvokeLua.ICalc), XLuaTestInvokeLuaICalcBridge.__Create);
    
}

XLuaTestInvokeLuaICalcBridge是继承自ICalc接口的类,它负责实现ICalc的功能,也就是我们一开始提到的一个PropertyChanged的event +=和-=操作,一个Add方法,一个Multi属性,以及下标操作符。__Create方法就是简单了返回了一个XLuaTestInvokeLuaICalcBridge对象:

public class XLuaTestInvokeLuaICalcBridge : LuaBase, XLuaTest.InvokeLua.ICalc
{
    public static LuaBase __Create(int reference, LuaEnv luaenv)
    {
        return new XLuaTestInvokeLuaICalcBridge(reference, luaenv);
    }
}

有了ICalc对象后,我们再次回到例子中,例子中接下来调用了Add方法与Multi的set属性,XLuaTestInvokeLuaICalcBridge类对它们的实现都比较简单,这里就不再赘述了。接下来是下标访问,对于get来说会去尝试访问lua层的get_item函数,而对于set来说则会去访问lua层的set_item函数。例子里还往PropertyChanged事件中注册了一个Notify方法,这时则会触发lua层的add_PropertyChanged函数,把C#的Notify方法push到lua层。

上一节我们提到,把C#对象push到lua层时,会调用到xlua的getTypeId方法,用来获取表示对象类的唯一ID,对于Notify方法来说,它就是一个委托,而委托实质上使用的是同一个type id:

if (typeof(MulticastDelegate).IsAssignableFrom(type))
{
    if (common_delegate_meta == -1) throw new Exception("Fatal Exception! Delegate Metatable not inited!");
    TryDelayWrapLoader(L, type);
    return common_delegate_meta;
}

TryDelayWrapLoader我们上一节分析过,这里就不展开了,由于没有wrap,还是通过反射生成类的各种table。最终lua层缓存了一个表示C# Notify方法的userdata。

此时再对table进行set_item,就会触发Notify方法调用了,对于delegate来说,xlua在初始化时就往metatable里设置了__call元方法:

public void CreateDelegateMetatable(RealStatePtr L)
{
    Utils.BeginObjectRegister(null, L, this, 3, 0, 0, 0, common_delegate_meta);
    Utils.RegisterFunc(L, Utils.OBJ_META_IDX, "__call", StaticLuaCallbacks.DelegateCall);
    Utils.RegisterFunc(L, Utils.OBJ_META_IDX, "__add", StaticLuaCallbacks.DelegateCombine);
    Utils.RegisterFunc(L, Utils.OBJ_META_IDX, "__sub", StaticLuaCallbacks.DelegateRemove);
    Utils.EndObjectRegister(null, L, this, null, null,
            typeof(System.MulticastDelegate), null, null);
}

[MonoPInvokeCallback(typeof(LuaCSFunction))]
public static int DelegateCall(RealStatePtr L)
{
    try
    {
        ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
        object objDelegate = translator.FastGetCSObj(L, 1);
        if (objDelegate == null || !(objDelegate is Delegate))
        {
            return LuaAPI.luaL_error(L, "trying to invoke a value that is not delegate nor callable");
        }
        return translator.methodWrapsCache.GetDelegateWrap(objDelegate.GetType())(L);
    }
    catch (Exception e)
    {
        return LuaAPI.luaL_error(L, "c# exception in DelegateCall:" + e);
    }
}

GetDelegateWrap方法就是根据委托的类型,反射取出它的Inovke方法,然后包装到MethodWrap的Call方法中,进行最终的反射调用。

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

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

相关文章

算法通关村第十关-青铜挑战快速排序

大家好我是苏麟,今天带来快速排序 . 快速排序 单边快速排序(lomuto 洛穆托分区方案) 单边循环 (lomuto分区) 要点 : 选择最右侧元素作为基准点j 找比基准点小的&#xff0c;i 找比基准点大的&#xff0c;一旦找到&#xff0c;二者进行交换。 交换时机: 找到小的&#xff0c…

第四篇 《随机点名答题系统》——基础设置详解(类抽奖系统、在线答题系统、线上答题系统、在线点名系统、线上点名系统、在线考试系统、线上考试系统)

目录 1.功能需求 2.数据库设计 3.流程设计 4.关键代码 4.1.设置题库 4.1.1数据请求示意图 4.1.2选择题库&#xff08;index.php&#xff09;数据请求代码 4.1.3取消题库&#xff08;index.php&#xff09;数据请求代码 4.1.4业务处理Service&#xff08;xztk.p…

AlphaControls控件TsDBCombobox出错:访问违规

日常使用AlphaControls控件TsDBCombobox&#xff0c;作为数据变化数据的控件。通常正常使用&#xff0c;一日 发现&#xff0c;出现以下错误&#xff1a; 控件访问违规的源代码&#xff0c;出错代码&#xff1a; function TacMainWnd.CallPrevWndProc(const Handle: hwnd; co…

基于Zemax的高能激光发射系统的扩束系统设计

关键词&#xff1a;高功率激光发射系统&#xff1b;扩束系统 1 引言 高功率激光发射系统是强激光空间传输系统中不可缺少的装置。对高功率激光发射系统的研究一直是激光应用领域的关键技术问题。高功率激光发射系统通常由准直系统、导光光路系统和扩束系统组成,光学系统要求具…

股票价格预测 | Python实现基于CNN卷积神经网络的股票预测模型(keras,Conv1D)

文章目录 效果一览文章概述源码设计参考资料效果一览 文章概述 股票价格预测 | Python实现基于CNN卷积神经网络的股票预测模型(keras) 源码设计 import quandl import datetimedf = quandl

Zabbix5.0部署

环境 主机名 IP 类型server01192.168.134.165zabbix-serverserver02 192.168.134.166zabbix-agent 官方部署文档 1 .安装yum源 [rootserver01 ~]# rpm -Uvh https://repo.zabbix.com/zabbix/5.0/rhel/7/x86_64/zabbix-rel…

分布式服务与分布式框架

分布式副武其实就是根据某个粒度&#xff0c;将服务拆分&#xff0c;而分布式框架就是将这些服务协调&#xff0c;管理起来。分布式框架&#xff0c;我认为服务调用是他的基础能力&#xff0c;该能力是所有分布式框架的基础能力&#xff0c;其次是服务注册与发现。 在这个维度…

OpenAI GPT-4 Turbo发布:开创AI新时代

&#x1f3a5; 屿小夏 &#xff1a; 个人主页 &#x1f525;个人专栏 &#xff1a; IT杂谈 &#x1f304; 莫道桑榆晚&#xff0c;为霞尚满天&#xff01; 文章目录 &#x1f4d1;前言一. GPT-4 Turbo的突破1.1上下文长度和控制手段的加强&#xff1a;1.2多模态支持&#xff1a…

ChatGpt3.5已经应用了一段时间,分享一些自己的使用心得.

首先ChatGpt3.5的文本生成功能十分强大&#xff0c;但是chatgpt有一些使用规范大家需要注意&#xff0c;既然chat是一种工具&#xff0c;我们就需要学会它的使用说明&#xff0c;学会chatgpt的引用语句&#xff0c;会极大的方便我们的使用。我们需要做以下的准备。 明确任务和目…

基于Vue+SpringBoot的厦门旅游电子商务预订系统 开源项目

项目编号&#xff1a; S 030 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S030&#xff0c;文末获取源码。} 项目编号&#xff1a;S030&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 景点类型模块2.2 景点档案模块2.3 酒…

ubuntu中用docker部署jenkins,并和码云实现自动化部署

1.部署jenkins docker network create jenkins docker run --name jenkins-docker --rm --detach \--privileged --network jenkins --network-alias docker \--env DOCKER_TLS_CERTDIR/certs \--volume jenkins-docker-certs:/certs/client \--volume jenkins-data:/var/jen…

vite2.9.15版本不显示el-table致命问题

1.版本说明 说明&#xff1a;vite版本为2.9.15&#xff1b;element-ui版本为2.15.14。 2.不显示 3.降低elementui版本 说明&#xff1a;不兼容&#xff0c;降低elementui版本为2.8.2 npm i element-ui2.8.2 4.显示

PS学习笔记——初识PS界面

文章目录 PS界面 PS界面 我使用的是PS2021&#xff0c;可能不同版本界面有所不同&#xff0c;但大体来说没有太多差异 可以看到下面这个图就是ps的主界面&#xff0c;大体分为菜单栏、选项栏、工具栏、面板、以及最中央的工作区。 ps中的操作基本都能在菜单栏中找到 可以从菜…

数位和相等数对的最大和【教3妹学编程-算法题】数位和相等数对的最大和

3妹&#xff1a;2哥&#xff0c;你有没有看到新闻“18岁父亲为4岁儿子落户现身亲子鉴定” 2哥 : 啥&#xff1f;18岁就当爹啦&#xff1f; 3妹&#xff1a;确切的说是14岁好吧。 2哥 : 哎&#xff0c;想我30了&#xff0c; 还是个单身狗。 3妹&#xff1a;别急啊&#xff0c; 2…

python计算脚长 青少年电子学会等级考试 中小学生python编程等级考试一级真题答案解析2022年9月

目录 python字符串输出 一、题目要求 1、编程实现 2、输入输出 二、解题思路 1、案例分析 三、程序代码 四、程序说明 五、运行结果 六、考点分析 七、 推荐资料 1、蓝桥杯比赛 2、考级资料 3、其它资料 python字符串输出 2022年9月 python编程等级考试一级编程…

小米真无线耳机 Air 2s产品蓝牙配对ubuntu20.04 笔记本电脑

小米真无线耳机 Air 2s产品蓝牙配对ubuntu20.04 笔记本电脑 1.我的笔记本是 22款联想拯救者y9000k&#xff0c;安装了双系统&#xff0c;ubuntu20.04。 2.打开耳机&#xff0c;按压侧面按钮2秒&#xff0c;指示灯显示白色闪烁。 3.打开ubunru20.04 系统右上角wifi的位置&…

基于ssm的房屋租售网站(有报告)。Javaee项目,ssm项目。

演示视频&#xff1a; 基于ssm的房屋租售网站(有报告)。Javaee项目&#xff0c;ssm项目。 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。 项目介绍&#xff1a; 采用M&#xff08;mode…

Leetcode 【2342. 数位和相等数对的最大和】

给你一个下标从 0 开始的数组 nums &#xff0c;数组中的元素都是 正 整数。请你选出两个下标 i 和 j&#xff08;i ! j&#xff09;&#xff0c;且 nums[i] 的数位和 与 nums[j] 的数位和相等。 请你找出所有满足条件的下标 i 和 j &#xff0c;找出并返回 nums[i] nums[j]…

Guitar Pro2024吉他软件好不好用?

吉他&#xff0c;这把魔幻的弹奏利器&#xff0c;既需要技术&#xff0c;又需要技巧。 是的&#xff0c;它不会自己跳入你的手中&#xff0c;除非你敲对了密码&#xff1a;练习&#xff01; 今天就来看看&#xff0c;大家是不是已经找到了快速掌握吉他的门道呢&#xff1f; …

企业如何实现降本增效——数字化转型

说到企业数字化转型&#xff0c;不可避免要围绕企业降本增效。企业们都在积极寻找降本增效解决之道&#xff0c;以实现降本增效的目标。数字化转型也成为了很多企业降本增效的重要手段。通过引入云计算、大数据、人工智能等技术&#xff0c;企业们实现了业务流程的数字化和智能…