深入理解C#的协变和逆变及其限制原因

news2024/12/25 14:12:49

阅读本文需要的一些前置知识:
C#基本语法、C#的泛型使用、C#的运行过程
由于协变和逆变存在一些细节,在阅读时请注意“接口”和“类型”的差异,此外,文中有可能在不同的语境中将“结构体”和“值类型”混用,但表达的同一个意思。

协变和逆变是一个C#的泛型开发中经常会遇到但可能意识不到的问题,往往会遇到一些认为应该可以但结果却发生了编译器报错表示类型不正确的情况,虽然有一些其他的方式绕过一些限制,但理解了协变和逆变可以在遇到错误时清晰地认识到问题的所在,也有助于写出更健壮的代码。

从一个例子开始

从一个在平时开发中可能遇到的问题下始,看下面几行代码。

string str = "";
object obj = str;//通过编译
string[] strArray = new string[1];
object[] objArray = strArray;//通过编译
List<string> strList = new List<string>();
List<object> objList = strList;//编译器报错 Cannot convert source type 'System.Collections.Generic.List<string>' to target type 'System.Collections.Generic.List<object>'

我们知道,C#中所有类型都是从object派生的,因此下面object obj = str;是符合逻辑的,同样object[] objArray = strArray也是符合预期的,但是为什么使用泛型List<object> objList = strList;时却无法通过编译呢?
这就是本篇文章的主题:变体(Variance),上面的object[] objArray = strArray形式被称为协变,此外还有另一种变体形式被称为逆变(一些书籍或文章翻译为抗变)。
变体的主要形式包括协变(Covariance)逆变(Contravariance),后面我会使用What-Why-How的方式逐个说明这两个概念。

协变(Covariance)

What-什么是协变

  1. 范畴学定义
    假定有两个类型X和Y,并且X的每个实例都能转换为Y类型(类似于C#中的继承/实现,即X是Y的派生类型)。如果对于一个与X和Y相关的类型I<X>和I<Y>,每一个I<X>的实例都能转换为I<Y>类型,那么就说I<>是协变的。
  2. C#举例说明
    定义比较干巴巴,举个实例来看看可以更清晰,还是上面那一段代码:
    string[] strArray = new string[1];
    object[] objArray = strArray;//通过编译
    
    stringobject的派生类,每一个string[]都可以转换为object[],这个string[]转换为object[]的过程就称为协变,也可以说“数组是协变的”。

Why-为什么要使用协变

1. 协变的使用场景

为什么要提出来协变的概念呢?事实上这不仅是一个非常正常的思考逻辑,而且还具有一定的实用意义。
假设这样一个场景:
我们有一个Log函数,需要使用它来格式化一系列对象的输出,要求它能支持所有类型的对象。假设我们使用的是泛型List,那么很自然地,我们的函数可能会写成下面这样:

string Log(List<object> outputList)
{
     string output = "";
     foreach (var obj in outputList)
     {
         output += obj.ToString();
     }

     return output;
}

当我们有一个List想要输出时,就会使用下面这样的代码:

 List<string> strList = new List<string>();
 string output = Log(strList);//Argument type 'System.Collections.Generic.List<string>' is not assignable to parameter type 'System.Collections.Generic.List<object>'

遗憾的是,如果真的这么写,就无法通过编译了,因为List<>这个泛型类并不支持协变。如果我们真的要使用List<>来格式化,就需要为每个不同的类写不同的重载,很明显这是不可行的。(其实可以使用string Log<T>(List<T> outputList)的签名来实现,但这是泛型方法的话题了。)
但同样明显的是,在这个案例中这样的使用方式是类型安全的,那么为什么C#要禁止协变呢?

2. 协变会带来什么问题

从例子中最容易看明白,直接上代码。

string[] strArray = new string[1];
object[] objArray = strArray;//通过编译
objArray[0] = new object();//运行时报错:System.ArrayTypeMismatchException

由于数组是支持协变的,我们构造一个string[]并且将其赋值组object[],然后向数组中插入一个object。此时会发现可以通过编译,但在运行时会抛出System.ArrayTypeMismatchException错误,插入的object类型不匹配。
在构造数组时,我们所获得的是一个string[],如果没有将其转换为object[],插入一个object很明显是不正确的,但经过协变转换后,它却通过了编译。
也就是说,在这种情况下的协变会导致一个运行时错误

3. 什么情况下可以使用协变

对比上面两个例子,我们知道协变有其特定的用处,但在一些情况下并不能保证程序的正确,所以协变只能在一些受限的情况下使用。在第一个例子里,我们需要的是对List<>进行读取并且确认是类型安全的,而第二个则是需要对实便行修改造成了程序错误。因此C#将泛型的协变限制为仅在读取时可用。

How-如何在C#中使用协变-out关键字

1. out标记

那么要怎么实现现读取和修改分开呢,其实只要能保证被转换后的类型只能被读取而不能被修改,那么就可以安全地使用协变了。
于是C#设计者采用了out关键字用于标记协变的方案,当使用这个关键字时,表明这个被修饰的类型仅被用于输出,并且不能被传入。在编译时,编译器会主动检查被标记类型参数的所有调用,确保它不会被作为传入类型使用。
在C#中就有这样的一个接口:IReadOnlyList<>,它的声明如下:

  public interface IReadOnlyList<out T> : IReadOnlyCollection<T>, IEnumerable<T>, IEnumerable
  {
    T this[int index] { get; }
  }

可以看到在它的类型参数前使用了out进行标记。如果再深入查看,会发现它继承的两个接口IReadOnlyCollection<T>IEnumerable<T>的类型参数也都是进了行out标记的。
我们经常使用的List<>就实现了这个接口:

  public class List<T> : 
    IList<T>,
    ICollection<T>,
    IEnumerable<T>,
    IEnumerable,
    IList,
    ICollection,
    IReadOnlyList<T>,//实现接口
    IReadOnlyCollection<T>
    {
        public T this[int index]
        {
            get
            {
                //具体实现
            }
        }
       //...其他成员实现    
    }

因此我们一开始的代码可以这样写:

    string Log(IReadOnlyList<object> outputList)//注意接口类型
    {
        string output = "";
        foreach (var obj in outputList)
        {
            output += obj.ToString();
        }

        return output;
    }

    //调用代码
    List<string> strList = new List<string>();
    string output = Log(strList);

在这里,strList协变成为了IReadOnlyList<object>

2. 协变的一些限制

虽然看起来只需要用out进行标记就可以实现协变,但在实际使用中还存在一些其他需要注意的地方,具体的原因我会放到文章最后详细说明。

  1. 在C#中,只有泛型接口泛型委托是协变的,泛型类和泛型结构体不是
    泛型是否协变举例
    泛型接口IReadOnlyList<object> lrol = new List<string>()
    泛型委托 Func<string> funcStr = () => ""; Func<object> funcObj= funcStr;
    泛型类不是|
    泛型结构体不是|
  2. 泛型的类型参数中类(class)是协变的,结构体(struct)不是。
    泛型是否协变举例
    泛型类参数IReadOnlyList<object> lrol = new List<string>()
    泛型结构体参数不是注意不可用IReadOnlyList<object> lrol = new List<int>()
  3. 必须显式声明协变,即标记out。注意由于第一条的存在,不能对class或者struct使用out关键字。
  class Pair<out T>{}
  //会提示编译错误,Variant type parameters could be declared in interfaces or delegates only(变体类型参数仅能被用于接口或委托声明)

逆变(Contravariance)

What-什么是逆变

  1. 范畴学定义
    假定有两个类型X和Y,并且X的每个实例都能转换为Y类型(类似于C#中的继承/实现,即X是Y的派生类型)。如果对于一个与X和Y相关的类型I<X>和I<Y>,每一个I<Y>的实例都能转换为I<X>类型,那么就说I<>是逆变的。(注意加粗的文本,与协变正好相反。)
  2. C#举例说明
    逆变相比协变要更难理解一些,还是举个实例来看看。
    Action<object> objAction = o => { o.GetType(); };
    Action<string> strAction = objAction;//可以通过编译
    
    stringobject的派生类,在这里objActionAction<object>的实例,在调用时需要传入一个object类型的实参。当我们向objAction中传入一个string对象时,由于stringobject的派生类,那么理所应当是可以被调用的。
    但是如果放到Action<>上来看,则是反过来了——与类型的派生方向相反,Action<object>对象转换成了一个Action<string>
    正因为与派生关系相反,因此叫它逆变

Why-为什么要使用逆变

1. 逆变的使用场景

上面的例子已经足以说明逆变的作用了,如果不允许逆变的存在,那么在Action<string> strAction = objAction;这样的代码就不能存在,这明显是不合理的,因为string派生自objectobject可以使用的成员在string中必然也存在。
前面使用了委托作为案例,下面再举一个接口的例子。
假设我们有一个Graph类及从它派生的CirleTriangle类,然后建立一个对Graph进行绘制的接口IDrawer<T>和对IDrawer<T>的实现Drawer。

    class Graph { }
    class Circle : Graph { }
    class Triangle : Graph { }

    interface IDrawer<T> where T : Graph
    {
        void DrawGraph(T graph);
    }

    class Drawer : IDrawer<Graph>
    {
        public void DrawGraph(Graph graph)
        {
            //具体实现
        }
    }

由于CircleTriangle都派生自Graph,因此IDrawer<Graph>DrawGraph都可以正常将TriangleCircle的实例作为参数,所以凡是需要一个IDrawer<Circle>的地方,都可以使用IDrawer<Graph>来代替,而不需要为每一个派生自Graph的都单独声明一个实现类。如果没有逆变,下面的调用将出现错误,但根据我们的分析,这是安全的。

    IDrawer<Graph> graphDrawer = new Drawer();
    graphDrawer.DrawGraph(new Triangle());//安全调用
    graphDrawer.DrawGraph(new Circle());//安全调用

    IDrawer<Circle> circleDrawer = graphDrawer;//需要逆变,编译时会发生错误,但实际上是类型安全的
    circleDrawer.DrawGraph(new Circle());//安全调用

    IDrawer<Triangle> triangleDrawer = graphDrawer;//需要逆变,编译时会发生错误,但实际上是类型安全的
    triangleDrawer.DrawGraph(new Triangle());//安全调用

2. 逆变可能带来哪些问题

与协变一样,如果不加限制地允许逆变,同样会带来一些问题,看下面这个例子:

    List<object> objList = new List<object>() { new object() };
    List<string> strList = objList;//逆变,编译时会发生错误
    string str = strList[0];//类型不匹配,必然出错

很容易看出,objectList中唯一的对象是一个object,无法作为string,运行时一定会出错。所以在这种情况下,即使在编译时允许了逆变,运行起来也会出错。

3. 什么情况下可以允许逆变

现在我们遇到了和协变一样的问题,在一些情况下是应该允许的(作为调用的实参),但在另一些情况下又不能允许(作为返回的结果),所以限制的方式也呼之欲出:当只作为传入参数时允许逆变

How-如何使用逆变

1. 使用in标记逆变

作为协变的反方向变化,C#同样提供了类似的方式来对泛型类型参数进行标记和限制:in关键字。
以上面的绘制代码为例,由于我们需要允许IDrawer的逆变,那么就需要在IDrawer接口上增加关键字in,如下面这样:

    class Graph { }
    class Circle : Graph { }
    class Triangle : Graph { }

    interface IDrawer<in T> where T : Graph//注意这一行在T前增加了in关键字
    {
        void DrawGraph(T graph);
    }

    class Drawer : IDrawer<Graph>
    {
        public void DrawGraph(Graph graph)
        {
            //具体实现
        }
    }

只需要进行这一个修改就可以允许逆变了。

2. 逆变的限制

逆变的限制与协变基本相同。

  1. 在C#中,只有泛型接口泛型委托是逆变的,泛型类和泛型结构体不是
  2. 泛型的类型参数中类(class,即引用类型)是逆变的,结构体(struct,即值类型)不是。
  3. 必须显式声明逆变,即标记in。

协变和逆变在C#中受限制的原因

在前面两部分中都提到协变和逆变受到了限制,但为什么会有这样的限制?主要原因是公共语言运行时(Common Language Runtime, CLR)的泛型机制造成的。

1. 协变和逆变本质上是隐式类型转换

协变和逆变本质上是隐式类型转换,这个原因导致了泛型类和结构体无法协变和逆变。
理解这一点非常重要,我们知道在非泛型的情况下,两个不同类型想要隐式转换,只有两种情况,一是存在隐式转换的函数,例如intdouble;二是存在继承或实现关系,例如stringobjectIReadOnlyList<>List<>

//存在隐式转换
int a = 1;
double b = a;

//存在继承关系
string str = "";
object o = str;

按这个关系,我们再来考虑泛型类List,使用下面两行代码进行输出,会发现List<object>List<string>并不属于同一个类。

Console.WriteLine($"typeof(List<object>)={typeof(List<object>)}");
Console.WriteLine($"typeof(List<string>)={typeof(List<string>)}");

//输出:
//typeof(List<object>)=System.Collections.Generic.List`1[System.Object]
//typeof(List<string>)=System.Collections.Generic.List`1[System.String]

很明显,List<object>List<string>既不存在继承关系,也不可能存在隐式转换函数,类型本就不一样,自然无法进行转换。
当然,这并不能说明为什么接口和委托可以进行转换,请继续往下看。

2. 泛型的CLR运行机制是类型参数为结构体的泛型接口/委托不能逆变和协变的根本原因

既然协变和逆变的本质是隐式类型转换,那如果一个值类型实现了接口,不就有了继承/实现关系吗?那为什么又会有值类型的泛型接口/委托不能逆变和协变的限制呢?
同样地,对于接口和委托而言,协变和逆变也不存在继承或实现关系,为什么却可以进行类型转换呢?难道它们之间存在什么特殊的隐式转换吗?

我们来看下面这个简单的例子,结构体People实现了接口IWalk,然后对构造一个泛型委托Action<IWalk>并向Action<People>逆变。如果进行编译,编译器会对逆变这一行代码提示错误。

    interface IWalk { }
    struct People : IWalk { }

    /
    
    Action<IWalk> aWalk = (walk) => { };
    Action<People> aPeople = aw;//实现了接口的结构体逆变,无法通过编译

造成这个限制的原因是CLR的运行机制。我们在C#中写的泛型会通过以下几个步骤来运行:

  1. C#静态编译为IL代码

  2. CLR创建开放的泛型类,并在调用处将类型参数传入

  3. CLR根据传入参数成为完整的类型。
    对于值类型,每个不同的类型参数创建不同的类,也就是说Action<int>Action<long>在CLR中会构造两个不同的类;
    对于引用类型,每个类型参数都使用object作为类型,也就是说Action<string>Action<object>在CLR中只会构造一个Action<object>类,并且都使用它,这是因为引用类型本质上只是一个指针。
    三个步骤可以参考下面的分支图

    静态编译
    值类型
    引用类型
    C#泛型
    IL泛型模板+调用时类型参数
    参数类型
    不同的值类型创建不同的泛型类
    所有引用类型都使用object作为类型参数

基于这个运行方式,我们可以发现,对于同一个泛型接口或委托而言,任何一个值类型生成的泛型都不可能和引用类型生成的泛型类型相同;而对于一个引用类型而言,它们在CLR中使用的始终是同一个类,只要编译器静态验证是合法的类型,那么就可以进行类型的转换。
在上面的代码中,对于aWalk而言,它在CLR中使用的是Action<object>这个类,而aPeople则是Action<object>,同样不存在继承关系,而C#也没有提供泛型的自定义隐式转换方式,所以自然也无法进行转型了。

现在让我们再来思考为什么接口和委托可以进行逆变或协变,我们知道在C#中为接口和委托提供了inout关键字进行标记,由于声明该接口或委托可以被用于协变或逆变。事实上在CLR运行时并不会验证这些类是不是匹配的(严格来说还是会验证,当类型出错还是会报错),因为对于CLR而言,它们都object类的泛型,这个报错只是由C#编译器进行判断接口或委托的内容是不是匹配。

3.必须显式使用in或out进行标记的原因

C#既然可以进行语法分析,自然也可以识别一个接口或委托是否支持协变或逆变,为什么不在识别到某个类需要协变或逆变的时候自动加上呢?这是因为自动识别有可能改变原有的设计意图
还是以之前的Graph-Drawer为例,假设我们新增了一个DrawerInvoker类,它传入一个IDrawer<Graph>对象,用于之后调用Drawer。

class Graph { }
class Circle : Graph { }
class Triangle : Graph { }

interface IDrawer<T> where T : Graph//没有标记协变或逆变
{
    void DrawGraph(T graph);
}

class Drawer : IDrawer<Graph>
{
    public void DrawGraph(Graph graph)
    {
        //具体实现
    }
}

class DrawerInvoker 
{ 
    public DrawerInvoker(IDrawer<Graph> drawer) 
    {
        //具体实现
    }
}

//-------------调用1----------
Drawer<Graph> graphDrawer = new Drawer<Graph>();

IDrawer<Circle> circleDrawer = graphDrawer;//需要逆变
circleDrawer.DrawGraph(new Circle());

DrawerInvoker drawerInvoker = new DrawerInvoker(circleDrawer);//需要协变

在上面这个例子中,假设我们同时写了需要逆变和需要协变的代码,编译器就无法确认IDrawer到底需要标记in还是out了——而且很明显,同一个接口的同一个类型参数并不能既协变又逆变。
更可怕的是,如果接口和调用在两个不同的程序集中,这还会影响接口定义处的逆变/协变性!也就是说,程序集外其他人的代码有可能改变程序集内的代码属性,这种情况明显存在巨大的安全风险。

到这里本篇文章的全部内容就都结束了,后面是一个简单的思维导图,希望可以对理解有一些帮助。

关于协变和逆变的思维导图

在这里插入图片描述

参考文献
C#本质论 第四版 C#5.0
.Net CLR Via C#
泛型接口中的变体 (C#)

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

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

相关文章

JavaEE简单实例——MyBatis一对多关联映射的嵌套结果集查询

简单介绍&#xff1a; 在之前的章节&#xff0c;我们简单介绍了MyBatis中的一对一的关联查询&#xff0c;使用了嵌套查询和嵌套结果集两种方式进行讲解&#xff0c;但是在实际的使用中&#xff0c;我们常用的是嵌套结果集的查询方式&#xff0c;所以在一对多的查询中&#xff…

无线耳机哪个品牌音质好?2023无线蓝牙耳机音质排行

现今&#xff0c;外出佩戴蓝牙耳机的人越来越多&#xff0c;各大品牌厂商对于蓝牙耳机各种性能的设计也愈发用心。那么&#xff0c;无线耳机哪个品牌音质好&#xff1f;下面&#xff0c;我来给大家推荐几款音质好的无线蓝牙耳机&#xff0c;可以当个参考。 一、南卡小音舱蓝牙…

【人工智能】科大讯飞API接口调用(第一集)

前言 这学期有一门人工智能教育课程&#xff0c;恰巧又有这么一个实践&#xff0c;要求进行人工智能接口调用 于是首选了科大讯飞&#xff0c;下面是详细过程记录 科大讯飞接口调用 以下是流程以及实现细节描述 调用流程 第一步 来到科大讯飞开放平台 http://www.xfyun.…

四、阻塞队列

文章目录基础概念生产者消费者概念JUC阻塞队列的存取方法ArrayBlockingQueueArrayBlockingQueue的基本使用生产者方法实现原理ArrayBlockingQueue的常见属性add方法实现offer方法实现offer(time,unit)方法put方法消费者方法实现原理remove方法poll方法poll(time,unit)方法take方…

wpf -绑定

事件双向驱动滑块变化&#xff0c;将值赋给文本控件与控件之间双向绑定{Binding ElementNameslider, PathValue}ElementName: 绑定哪个控件呢&#xff1f; 指绑定的x:Name这个控件Path &#xff1a; 绑定哪个属性呢&#xff1f;Value<StackPanel><Slider x:Name"…

Python每日一练(20230227)

目录 1. 路径交叉 ★★★ 2. 缺失的第一个正数 ★★★ 3. 寻找两个正序数组的中位数 ★★★ 附录 散列表 基本概念 常用方法 1. 路径交叉 给你一个整数数组 distance 。 从 X-Y 平面上的点 (0,0) 开始&#xff0c;先向北移动 distance[0] 米&#xff0c;然后向西移…

ChatGPT提高你日常工作的五个特点,以及如何使用它来提高代码质量

ChatGPT已经完全改变了代码开发模式。然而&#xff0c;大多数软件开发者和数据专家们仍然不使用ChatGPT来完善——并简化他们的工作。 这就是我们在这里列出提升日常工作效率和质量的5个不同的特点的原因。 让我们一起来看看在日常工作中如何使用他们。 警告&#xff1a;不要…

第十一届“泰迪杯”数据挖掘挑战赛赛前指导安排

第十一届“泰迪杯”挑战赛报名一周了&#xff0c;许多的参赛队伍及带队老师都在咨询我们赛前指导安排及内容&#xff0c;今年的赛前指导安排还是分为了赛前指导录播课程及赛前指导直播两个模块。小编这就为大家介绍一下吧。 赛前指导 赛前指导录播课程 2月25日9:00-4月14日 …

vue中的百度地图的搜索定位功能

效果图 申请百度地图AK 前往 百度地图开放平台控制台 &#xff0c;登录百度账号&#xff0c;创建应用即得。 封装loadBMap.js文件 /*** 动态加载百度地图api函数* param {String} ak 百度地图AK&#xff0c;必传*/ export default function loadBMap(ak) {return new Promise…

C语言|文件读写,代码运行后留下“记忆”

前言对于一个代码&#xff0c;运行时可能需要保留产生的结果&#xff0c;例如计算值&#xff0c;筛选值&#xff0c;记录点或者小游戏的得分&#xff0c;而正常情况下我们要保存一个数据&#xff0c;想到的肯定是打开我们的文本软件&#xff0c;手撸文字&#xff0c;今天这篇文…

Flutter 数据传递

在应用开发过程中数据传递&#xff0c;flutter提供 InheritedWidget 以及多种 provider, 各有差异从从使用习惯上面 这边主要介绍以下两种&#xff1a; InheritedWidgetprovider &#xff08;ChangeNotifier&#xff09;InheritedWidget&#xff1a; 提供一种 从上而下 的数据…

线上研讨会报名 | Perforce、中手游、星思半导体专家邀您一起畅聊如何通过数字资产管理与版本控制赋能大规模研发

全球领先的数字资产管理与DevSecOps工具厂商Perforce联合中国授权合作伙伴龙智举办的Perforce on Tour网络研讨会将于2月28日下午2:00举行。 本次研讨会以“赋能‘大’研发&#xff0c;助力‘快’交付”为主题&#xff0c;龙智董事长何明、Perforce高级顾问Robert Cowham&…

SpringMVC的基础知识以及如何使用各注解

1.SpringMVC的概述 学习SpringMVC我们先来回顾下现在web程序是如何做的&#xff0c;咱们现在web程序大都基于三层架构来实现。 三层架构 浏览器发送一个请求给后端服务器&#xff0c;后端服务器现在是使用Servlet来接收请求和数据 如果所有的处理都交给Servlet来处理的话&am…

SpringBoot (一) 项目构建、配置读取、静态资源定义

哈喽&#xff0c;大家好&#xff0c;我是有勇气的牛排&#xff08;全网同名&#xff09;&#x1f42e; 有问题的小伙伴欢迎在文末评论&#xff0c;点赞、收藏是对我最大的支持&#xff01;&#xff01;&#xff01;。 前言 SpringBoot是基于Spring开发的开源项目&#xff0c…

Apache 深入优化

Apache 深入优化 &#x1f3c6;荣誉认证&#xff1a;51CTO博客专家博主、TOP红人、明日之星&#xff1b;阿里云开发者社区专家博主、技术博主、星级博主。 &#x1f4bb;微信公众号&#xff1a;微笑的段嘉许 &#x1f4cc;本文由微笑的段嘉许原创&#xff01; &#x1f389;欢迎…

史上最全的大数据开发八股文【自己的吐血总结】

自我介绍 我本硕都是双非计算机专业&#xff0c;从研一下开始学习大数据开发的相关知识&#xff0c;从找实习到秋招&#xff0c;我投递过100公司&#xff0c;拿到过10的offer&#xff0c;包括滴滴、字节、蚂蚁、携程、蔚来、去哪儿等大厂&#xff08;岗位都是大数据开发&#…

阶段八:服务框架高级(第四章:Redis多级缓存案例)

阶段八&#xff1a;服务框架高级&#xff08;第四章&#xff1a;Redis多级缓存案例&#xff09;Day-Redis多级缓存案例0.学习目标1.何为多级缓存2.JVM进程缓存2.1.导入案例2.2.初识Caffeine 【重要】2.3.实现JVM进程缓存 【重要】2.3.1.需求2.3.2.实现3.Lua语法入门 【重要】3.…

idea 配置快捷生成类和方法注释,验证通过

1 Live Templates里新建模板组 . File-->Settings-->Editor--> Live Templates 属于组名,这里我选择MyComment 2. 新建类注释模板 2.1 选择上一步新建的模板组 2.2 编辑模板 (1)Abbreviation里输入想要的快捷键&#xff0c;这里我选择cl代表class; (2)Templates tex…

大数据之Phoenix基本介绍

文章目录前言一、Phoenix简介二、Phoenix入门&#xff08;一&#xff09;创建表语法&#xff08;二&#xff09;查看表信息&#xff08;三&#xff09;删除表&#xff08;四&#xff09;大小写问题前言 #博学谷IT学习技术支持# 上篇文章介绍了Phoenix环境搭建&#xff0c;点击…

IntelliJ插件开发教程之开发思路

JetBrains公司系列产品IDEA、WebStrom、PyCharm、CLion、GoLand等都是基于IntelliJ Platform开发而成&#xff0c;掌握IntelliJ插件开发技能便能拥有提升开发效率的终极武器。本教程Dmeo源码请关注微信公众号“开发效率”进行获取。如果您是JetBrains产品的用户&#xff0c;那您…