类型推断技术及仓颉语言实践
史磊
仓颉语言类型推断技术专家
一、一种看待类型系统的方式
一门编程语言一定得包含类型系统吗?
这个问题今天看来可能显而易见,一个程序没有类型的话还能算是个完整、正确的程序吗?但是其实关于类型系统的作用,一直是存在两种针锋相对的看法的。大家普遍习惯的这种把类型作为程序不可分割的一部分的看待方式叫做“内生”(intrinsic)的理解方式,即一个程序如果没有合法的类型就不能算有意义的程序。而与之相对的,也存在一种“外生”(extrinsic)的理解方式,持这种观点的人认为,程序即使没有类型也同样有意义,类型检查只是额外地证明了这个程序的一些性质,与其他的各种程序分析工具应该处于同样的地位。
姑且不论类型检查是否应该作为一个语言之外的的工具这种工程问题,顺着“类型检查是为了证明了程序的一些性质”这个思路,我们可以得到一些很有趣的认知。
不妨想象一下,一个程序如果在没有类型的情况下就执行起来,会发生什么问题呢?我们有可能给一个函数传入一个整数,但是这个函数实际上却是处理字符串用的,于是它错误地把这个整数形式地数据当做字符串来处理了,最后自然是得不到期望的结果。例如下面的伪代码:
func f(s) {s.replace(...)
}
let s = f(1)
而在这个过程中加入类型系统的话,如果可以静态地验证这个被调用的函数的形参类型是一个字符串,而每次调用它时实参类型都也是字符串,那么我们就可以保证,在任何情况下执行这个程序,都不会出现把一个整数类型的参数传递给这个处理字符串的函数这种低级错误。
所以这里我们用比较朴素的方式去理解的话,类型系统、尤其是静态类型系统,实际上是一种“程序在运行时处理数据的方式永远不会出错”的证明,如果类型检查可以通过,那么每个数据被传递时,它的提供者和接收者对于这块数据的格式、可以被执行的操作等等的理解总是一致的。实际上,这个性质已经几乎就是一些比较公认的“类型安全”(type safety)的定义了。
另外细心的读者可能会注意到,既然类型系统是一种程序运行时“永远不会出错”的证明,那这个证明完成后,在程序运行时,我们还需要保留“类型”这个东西吗?确实如此,除了为了支持动态派遣等一些动态特性外,大多数静态类型的信息在类型检查完成后对于程序的正确运行就基本没有用处了,理论上是可以被擦除掉的。当然,实际的实现中类型信息对编译优化也有帮助,所以也有语言会选择在后续的编译阶段一直保留它们。
那么我们再回头去看这篇文章最关心的重点问题:类型推断是什么?类型标注又是什么?既然类型检查是一种正确性的证明,那开发者提供的类型标注自然就是手写的部分证明,而类型推断则是一种自动化证明的技术。自动化证明的算法越聪明,必须手写的部分也就越少,能支持的类型特性也就越丰富。实际上,在一些研究性的语言中,类型系统早已超越简单的“处理数据的方式”的正确性证明,还可以包括资源消耗、执行时长、数组长度、值的可空性等等多种方面的正确性证明,其中的类型推断用到的技术与更一般的形式化证明也越来越接近了。
二、仓颉的类型推断需要做什么
在仓颉语言中,类型推断主要起到两个作用,第一是确定每个声明和表达式的类型,这既包括可以省略的变量类型、函数返回类型、泛型实参类型等等,也包括分支、循环、字面量等并不涉及类型标注的各种表达式的类型。第二是帮助确定每个被使用的名字对应的声明,最典型的情况是在调用一个重载的函数时,需要确定调用的具体是哪个定义,另外有些不同种类的定义,如果碰巧名字相同,我们也可能需要借助类型信息来确定使用的究竟是哪个。
三、基于合一(unification)的类型推断
在一些早期的编程语言中,类型之间并没有子类关系,只有相等或不等。时至今日,多数函数式编程语言仍然继承了这个设计思路,包括 Rust 语言如果不考虑生命周期的话,类型之间也没有子类关系。在这些语言中,类型推断普遍是基于合一(unification)过程来完成的。简单来说,即是每当我们遇到一个暂不确定的类型,那就为它引入一个待解的变元,然后在遍历程序的过程中,不断建立起类型变元之间的方程组(如一个函数的形参类型和实参类型必须相等,由此就可以得到一个新的方程),并不断简化这些方程,直到所有类型变元都可以求解出来(或者缺乏足够的信息来求解,此时就会将这样的类型变元泛化,不过这超出本文的讨论范围了),类型推断的过程十分类似于数学上解方程的过程。
这种做法的好处是,这些方程组是从整个程序里所有信息中建立起来的,所以每个未知类型都可以利用全局信息去求解,解出的结果也可以保证是全局正确的。
例如下面这个程序在 Rust 中是可以编译通过的,它可以根据第 5 行的函数调用解出变量 v
需要有 u16
类型。但是等价的程序在一些没有全局的类型推断的语言中则无法通过编译,因为在第 4 行中 v
的类型就已经被推断为整数字面量的默认类型了,这通常是 u64
。
fn f(x:u16) {}
fn main() {let v = 1;f(v);
}
那么为什么不是所有语言都用这种基于合一(unification)的类型推断方式呢?主要问题就出在子类关系上。
如果类型间不存在子类关系,那么所有类型实际上是一些离散的点,两个类型要么相等,要么不等,几乎没有歧义的空间。这样求解方程非常简单直接,结果也很容易保证完备性和可靠性。
但是有了子类关系,类型之间就不再是一些毫无关系的点了,而是形成了一个偏序集。从程序中可以建立起的也不再是基于类型相等的方程组,而是基于子类关系的不等式组。求解一个偏序集上的不等式组则是一个复杂得多的问题,很多时候我们甚至无法在可接受的时间内求得一个完全精确的解,只能退而求其次做一些近似。这就导致基于合一(unification)的类型推断在有子类关系的语言中非常难以使用,而仓颉语言是有子类关系的,所以也暂时没有采用这种类型推断方式。
四、局部类型推断
上面讲的的子类关系会导致的问题,曾一度导致在有子类关系的语言中做类型推断的研究非常稀少。对于注重实用的工业语言来说,比较重要的一篇研究论文是 Benjamin. C. Pierce 等人著的 《Local Type Inference》1。它不再纠结于全局的完全性和完备性,退而求其次只追求每个表达式的类型在相邻表达式能提供的信息中保证正确。仓颉的类型推断基本上是基于这篇论文的框架在实现。
更准确地来说,《Local Type Inference》这篇论文实际上包含了两个类型推断的思想,一个是仅依据局部信息做类型推断,也就是“局部类型推断”,另一个是类型信息应该在表达式中自底向上和自顶向下双向传播,也叫“双向类型推断”。这两者在仓颉的类型推断实现中都有所体现,下面就具体来介绍一下仓颉的类型推断方式:
局部元素
在一个块内部,编译器会按从上到下的顺序依次处理各个表达式和局部声明。
在一个表达式或局部声明的内部,编译器依据可获得的类型信息,会做自顶向下或者自底向上的推断。
自顶向下的推断是指,父表达式如果对子表达式有期望的类型,则我们会借助这个期望类型的信息去推断子表达式的类型。一个典型的情形是,在一个函数调用表达式中,我们总是期望实参的类型是对应位置形参的子类,因而可以用形参类型作为期望类型去帮助推断实参类型。其他还包括 if
表达式的条件需要是 Bool
类型的子类,变量的初始化表达式的类型需要是变量类型的子类等等。
下面举一些例子:
func handleByte(b: Byte) {
}
func f() {let v: Int32 = 1 // 1 inferred to Int32handleByte(2) // 2 inferred to Byte
let v2 = 3 // 3 inferred to Int64
}
上面的程序中,第5行的变量 v
有类型标注 Int32
,因此对其初始化表达式存在期望类型 Int32
,所以整数字面量 1
被推断为类型 Int32
。
第6行的 handleByte
函数有形参类型 Byte
,因此这里的调用表达式对它的实参就有期望类型 Byte
,所以整数字面量 2
被推断为类型 Byte
。
以上都是自顶向下的推断发生的场景,而第8行中则没有发生自顶向下的推断。变量 v2
没有标注类型,所以字面量 3
就不具有任何从父表达式得到的期望类型,只能被推断为整数字面量的默认类型 Int64
。
需要注意的是,自顶向下的期望类型在一些表达式中是可以向下传递的。例如,一个 if 表达式如果本身有期望类型,那么我们会认为它的各个分支同样有这个期望类型。
下面是一个例子:
func narrow(x: Int64):Byte {let v: Byte = if (x >= 0) {1 // 1 inferred to Byte} else {-1 // -1 inferred to Byte}return v
}
其中变量 v
标注了类型 Byte
,因此其初始化表达式、即 if 表达式会具有期望类型 Byte
。然后 if 的两个分支也都会传递地具有期望类型 Byte
。因而,第3行的 1
和第5行的 -1
字面量都将被推断为 Byte
类型。
除了 if 表达式以外,一个表达式的返回值如果可能来源于它的某个子表达式,那么对这个表达式的期望类型通常就可以传递给这个子表达式,如从 match 表达式到它的各个 case,从 try-catch 表达式到它的 try 分支和各个 catch 分支等等。
而自底向上的推断是指,如果父表达式对子表达式没有期望的类型,我们会先推断出子表达式的类型,并反过来用这个信息帮助推断父表达式的类型。一个典型的场景是调用泛型函数时,如果没有写出泛型参数,则通常需要先推断出实参的类型,然后才能依此求解出整个函数调用表达式的泛型参数,进而得到整个表达式的类型。
以下是一个简单的例子:
func id<T>(x: T): T {x
}
let s = id("hello") // will infer id<String>
其中定义了一个接受一个任意类型的参数并返回其自身的泛型函数 id
。第5行代码中,我们既不知道变量 s
的类型,也不知道调用的 id
函数的类型参数,所以只能先从最里层的表达式 "hello"
开始推断,得到它的类型为 String
。然后向上,利用实参类型为 String
,求解出此处调用 id
的泛型实参是 String
,进而确定它的返回类型也是 String
,最后再向上推断出变量 s
的类型是 String
。
泛型参数求解
在调用泛型函数、或者构造泛型类型时,我们需要知道本次调用的泛型实参。函数调用的泛型实参通常可以省略,此时编译器会尝试从上下文中推断出泛型实参的类型。在局部类型推断中,这是最为复杂的步骤,也是唯一实际上用了合一(unification)的步骤。
上文介绍过,在有子类关系的类型系统上合一(unification)需要求解不等式组。那么我们需要首先建立这些不等式。因为局部类型推断并不考虑全局信息,所以针对一个特定的泛型函数的调用,我们只会考虑下面几个不等式的来源:
-
函数的形参类型需要是实参类型的父类
-
函数的返回类型需要是本次调用的期望返回类型的子类
-
函数的泛型约束需要被满足
具体求解的过程略为复杂,本文不会详细介绍。大致上来说,对于一个泛型参数,编译器会尝试寻找能满足以上所有约束的唯一最具体类型、或唯一最抽象类型,来作为这个泛型参数的解。如果这样的类型无法找到,则会编译失败。另外,如果找到的类型只能是 Any
或者 Nothing
,则编译也会失败。
以下是一个简单的情形:
func tryPrint<T>(x: Option<T>) where T <: ToString {if (let Some(v) <- x) {println("Has value: ${v}")}
}
main() {tryPrint(Some(1)) // will infer tryPrint<Int64>
}
第8行代码中 tryPrint
函数的泛型参数需要推断。它的形参类型和实参类型间的对应关系为 Option<Int64> <: Option<T>
,此外它有泛型约束 T <: ToString
。结合两者,我们发现类型 Int64
可以作为 T
的解,所以此处 tryPrint
的泛型实参就被推断为 Int64
。
另外在一些较为复杂的情形下,泛型函数的实参之间可能有类型信息的依赖,无法用一次简单的自底向上或者自顶向下推断完成泛型参数的求解。这种情形在原版的《Local Type Inference》中并没有解决,一个比较有名的后续工作叫做《Colored Local Type Inference》2,它通过一个十分重量级的方法 -- 给类型中每个类型参数再更详细地标上需要自底向上还是自顶向下推断出来 -- 来实现对这种情形的支持。
仓颉参考了这种思路,但是并没有引入那么重量级的标记,只是会在泛型函数调用时尝试多次迭代求解泛型参数。以经典的 map
函数为例:
import std.collection.ArrayList
func map<T, R>(f: (T)->R, arg: ArrayList<T>): ArrayList<R> {let res = ArrayList<R>()for (v in arg) {res.append(f(v))}res
}
main(){let input: ArrayList<Int64> = ArrayList([1, 2, 3])let output = map({ x => x.toString() }, input) // will infer map<Int64, String>
}
map
函数接受一个转换函数和一个列表,将列表中的元素逐一转换后输出新的列表。因为有转换前后两种不同类型,所以它需要2个泛型参数 T
和 R
。
在第13行对 map
的调用中,泛型参数 T
可以很显然地从 input
参数的类型推断出来为 Int64
,但是对它的另一个参数,即作为转换函数的 lambda,在没有期望类型的情况下,我们无法简单确定这个 lambda 的参数和返回类型,因而也就无法解出 R
的类型。
但同时我们又可以观察到,这个 lambda 的参数类型和 input
的类型都对应着泛型参数 T
,因此他们应该相等。而知道lambda的参数类型后,他的返回类型就可以推断出来了。进而,R
的类型也可以求解出来。
所以总的来说,这种情形仍然是有足够的信息进行泛型参数的求解的,只不过需要跨越参数进行一些类型信息的传递。
对于这种参数之间有类型信息的依赖的泛型函数调用,我们的处理流程是这样的:
-
首先,推断出所有不需要期望类型就可以推断成功的实参的类型
-
然后用推断出的实参类型尽可能地求解泛型参数的类型
-
将可以求解出的泛型参数代入形参类型中,将这些(可能没有完全实例化的)形参作为期望类型去推断对应的实参类型,这个过程中,我们会忽略掉类型中没有完全实例化的部分
-
重复 2、3 步,直到没有新的泛型参数可以被求解(即失败),或者所有泛型参数都可以被求解(即成功)
最后还有一种较为特殊的场景,有些函数可能定义成柯里化的形式,即每次接收一个参数,返回接收下一个参数的函数。
例如,仓颉标准库中的 map
函数就提供了柯里化方式定义的版本:
public func map<T, R>(transform: (T) -> R): (Iterable<T>) -> Iterator<R> {return {it: Iterable<T> => it.iterator().map<R>(transform) }
}
柯里化的函数调用时,会分为多次函数调用:
let it = map({x=>x.toString()})([1,2,3])
当然,通常我们写成管道操作符的语法糖形式:
let it = [1, 2, 3] |> map{x=>x.toString()}
这种情形下,柯里化的泛型函数的多次调用是连续发生的,它泛型参数仍然可以被推断出来,编译器会将后面的调用中的实参类型信息以期望的返回类型的形式向前传播,帮助推断泛型参数。
也就是说,对于第一次对 map
的调用 map({x=>x.toString()})
,我们可以知道它的返回类型需要满足: (Iterable<T>) -> Iterator<R> <: (Array<Int64>) -> Any
。然后使用上面描述过的求解步骤,就可以解出 T
和 R
分别为 Int64
和 String
。
重载决议
仓颉语言支持函数和操作符的重载,在编译时,需要静态地做重载决议,以确定每个重载函数的调用究竟用了哪个定义。
仓颉做重载决议的过程大致上可以分为3步:
-
找出当前(调用)位置可见的候选定义
-
检查每个候选定义是否能通过调用处局部的类型检查
-
在所有可以通过类型检查的定义中,选取最匹配的一个
更具体的规则可以在用户手册或者语言规约中找到,这里就不再赘述。这里主要讨论2个在重载决议过程中值得一提的点。
第一点是,如果被调用函数的某些候选定义是泛型函数,且需要求解泛型参数,那么对每个候选定义会分别进行泛型参数的求解。
例如在下面的程序中,第8行对 f
的调用,若选取第1行的定义,则可以求解出泛型参数 T
为 String
,若选取第4行的定义,则可以求解出泛型参数 T
为 Int64
。两者皆可类型检查通过,所以会报错无法决议。
func f<T>(x: Int64, y: T) {
}func f<T>(x: T, y: String) {
}main() {f(1, "c") // Error, both candidates match, ambiguous
}
第二点则是因为上面提到的双向类型推断而衍生出的问题。
因为我们采用的双向类型推断策略,一个函数调用期望的返回类型对一个候选定义能否通过“局部的类型检查”会产生影响,因而可能改变重载决议的结果。也就是说,一个重载函数的调用,在不同上下文下,决议的结果是可能不同的。
例如下面的程序中,第10行的 f
会考虑到期望的返回类型是 Int32
而选择第5行的定义,而第11行的 f
会考虑到期望的返回类型是 Int64
而选择第1行的定义。
func f(x: Int64):Int64 {x
}func f(x: Int32):Int32 {x
}main() {let v1: Int32 = f(1) // will use definition of line 5let v2: Int64 = f(1) // will use definition of line 1
}
那么我们再考虑一种特殊的情况,如果有多层重载函数的调用嵌套起来,会发生什么事呢?因为外层函数的形参会被视为内层函数调用的期望类型,而外层函数的形参类型会依候选定义的选取而有所变化,所以外层对候选定义的选取同样会影响内层的重载决议。
例如下面的程序中,第10行外层的 widen
会考虑到期望的返回类型是 Int32
而选择第5行的定义,而内层的 widen
会考虑到外层期望的参数类型是 Int16
而选择第1行的定义,最后,字面量 1
会因为第一行的 widen
定义期望一个 Int8
类型的参数而被推断为 Int8
类型。程序整体可以通过类型检查。
func widen(x: Int8):Int16 {Int16(x)
}func widen(x: Int16):Int32 {Int32(x)
}main() {let v: Int32 = widen(widen(1)) // 1 inferred to Int8
}
这就产生了一个问题,重载函数嵌套层数较多的情形下(如比较大的UI组件、复杂的算术表达式等),我们做决议可能需要搜索各层的候选组合,而需要搜索的组合数可能随着嵌套层数而指数增长,这就可能让编译时间长到无法接受。实际上同样的问题也可以在 Swift 语言中观察到,例如至少在撰写本文时,Swift 的编译器仍然会因为如下的程序编译超时:
let a: Double = -(1 + 2) + -(3 + 4) + -(5)
这和 Swift 同样使用了双向类型推断不无关系。
那么仓颉是如何解决这个问题的呢?让我们回忆一下,一个函数的重载决议总共需要哪些信息?
-
当前的表达式
-
对当前表达式的期望类型
-
当前上下文中的符号表
其中对于源程序中同一个位置的表达式, 1 是完全不变的,3 可以近似认为不变,事实上会导致指数的搜索量的是 2 在不同上下文中会有所不同。看到这里,经常刷算法题的朋友可能可以敏锐地发现,在双向类型推断下做重载决议实际上几乎是一个动态规划问题。对于给定的程序位置和期望类型,只要上下文中的符号表不发生改变,那么当前位置的表达式能否通过类型检查就是一个具有最优子结构的问题。而所有可能的期望类型并不是很多,它最多等于父表达式调用的函数的候选定义的总数。
因此,我们可以在重载决议时做一个记忆化搜索,对同一个函数调用,如果用同样的期望类型已经做过类型检查,直接复用前一次检查时缓存的结果就好了。而在一些特殊情形下,如果符号表确实发生了变化,只要将受影响的缓存清除掉即可。如此一来,对于绝大多数程序,重载决议都可以在接近多项式的时间复杂度内完成。
六、小结
本文简单介绍了业界对类型系统和类型推断相关的讨论和已有工作,并介绍了在仓颉的实践中类型推断遇到的问题和解决的方式。仓颉使用了基于局部类型推断的算法框架,并针对一些常见的使用场景做了专门的优化,在推断能力、算法复杂度、以及支持的语言特性间达到了一个不错的平衡点。
七、参考文献
-
[1] Pierce B C, Turner D N. Local type inference[J]. ACM Transactions on Programming Languages and Systems (TOPLAS), 2000, 22(1): 1-44.
-
[2] Odersky M, Zenger C, Zenger M. Colored local type inference[J]. ACM SIGPLAN Notices, 2001, 36(3): 41-53.
相关文章:
类型推断技术及仓颉语言实践
史磊 仓颉语言类型推断技术专家 一、一种看待类型系统的方式 一门编程语言一定得包含类型系统吗? 这个问题今天看来可能显而易见,一个程序没有类型的话还能算是个完整、正确的程序吗?但是其实关于类型系统的作用,一直是存在两种…...
职场生存秘籍:16条黄金法则
作者简介:一名计算机萌新、前来进行学习VUE,让我们一起进步吧。 座右铭:低头赶路,敬事如仪 个人主页:我叫于豆豆吖的主页 写在前面 在这个瞬息万变的时代,职场不仅是实现个人价值与梦想的舞台,更是一…...
Flask 介绍
Flask 介绍 为什么要学 Flask框架对比设计哲学功能特点适用场景学习曲线总结 Flask 的特点Flask 常用扩展包Flask 的基本组件Flask 的应用场景官方文档官方文档链接文档内容概述学习建议 Flask 是一个使用 Python 编写的轻量级 Web 应用框架。它旨在让 Web 开发变得快速、简单且…...
JAVA基础知识点3 (String 和 StringBuffer 以及 StringBuilder 的特点以及区别)
1,String 和 StringBuffer 以及 StringBuilder 的特点 (1)String的特点:String是final修饰的字符序列是不可改变的, 是字符串常量,一旦初始化就不可以被更改,因此是线程安全的 因为是常量每次对其操作都会…...
2024年8月AI内容生成技术的现状与未来:从文生文到跨模态交互的全景分析
2024年8月AI内容生成技术的现状与未来:从文生文到跨模态交互的全景分析 大家好,我是猫头虎!🚀 随着AI在内容生成领域的爆发式发展,从2022年末开始,AI生成技术已经走过了文生文(AIGC)…...
File 34
package File;import java.awt.*; import java.io.File;public class file1 {public static void main(String[] args) {//创建FILE对象,指代某个具体的文件//路径分隔符File f1new File("C:/Users/SUI/Desktop/kaishi/nih.txt");// File f1new File(&quo…...
AI全知道-Embedding model中的Vector知识点
在嵌入模型(Embedding Model)中,向量(Vector)是核心概念之一。向量表示法不仅是数学中的基本工具,也是机器学习和深度学习中处理高维数据的关键手段。本文将深入探讨向量在嵌入模型中的作用、表示方法、计算和应用等知识点。 一、向量的基本概念 向量是一个具有方向和大…...
Qt 学习第四天:信号和槽机制(核心特征)
信号和槽的简介 信号和插槽用于对象之间的通信。信号和插槽机制是Qt的核心特征,可能是不同的部分大部分来自其他框架提供的特性。信号和槽是由Qt的元对象系统实现的。介绍(来自Qt帮助文档Signals & Slots) 在GUI编程中,当我们…...
跳跃游戏Ⅱ C++简单代码
给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i j] 处: 0 < j < nums[i] i j < n 返回到达 nums[n - 1] 的最…...
Gitlab中access token 和Deploy token的区别
在GitLab中,Access Token和Deploy Token是两种不同类型的令牌,用于不同的目的。以下是它们的主要区别: ### Access Token 1. **用途**: - 用于用户身份验证,允许用户以编程方式访问GitLab API。 - 可以用于克隆…...
【多线程】线程的五种创建方法
文章目录 线程在 Java 代码中编写多线程程序Thread 标准库 创建线程的写法1 . 继承 Thread 类代码回调函数休眠操作:sleep()抢占式执行观察线程jconsoleIDEA 内置调试器 2 . 实现 Runnable 接口代码 3. 匿名内部类创建 Thread ⼦类对象代码匿名内部类 4.匿名内部类创…...
关闭窗口工具类 - C#小函数类推荐
此文记录的是一个关于关闭窗口工具类。 /***关闭窗口工具类Austin Liu 刘恒辉Project Manager and Software DesignerE-Mail: lzhdim163.comBlog: http://lzhdim.cnblogs.comDate: 2024-01-15 15:18:00使用方法:CloseWindowUtil.CloseWindow(this.Handle);***/n…...
Xilinx FPGA 原语解析(一):IBUFDS_GTE3 差分时钟输入缓冲器
目录 1.使用说明 2.实例化代码 3.参数解释 4.端口连接 1.使用说明 IBUFDS_GTE3 是Xilinx FPGA 中用于高速接口的差分时钟信号输入缓冲器。 BUFDS_GTEx,x2/3/4(不同系列的FPGA x的值不同),其中UltraScale使IBUFDS_GTE3…...
力扣SQL50 患某种疾病的患者 正则表达式
Problem: 1527. 患某种疾病的患者 在SQL查询中,REGEXP 是用于执行正则表达式匹配的操作符。正则表达式允许使用特殊字符和模式来匹配字符串中的特定文本。具体到你的查询,^DIAB1|\\sDIAB1 是一个正则表达式,它使用了一些特殊的通配符和符号。…...
k8s集群的资源发布方式(滚动/蓝绿/灰度发布)及声明式管理方法
目录 1.常见的发布方式 2.滚动发布 3.蓝绿发布 4.实现金丝雀发布(Canary Release) 5.声明式管理方法 1.常见的发布方式 蓝绿发布:两套环境交替升级,旧版本保留一定时间便于回滚优点:用户无感知,部署和回滚速度较…...
SwiftUI 中掌握 ScrollView 的使用:滚动可见性
文章目录 前言视图修饰符应用场景可见性完整示例ContentViewVideoPlayerViewScrollViewVisibilityApp 总结 前言 我们的滚动 API 中又有一个重要的新增功能:滚动可见性。现在,你可以获取可见标识符列表,或者快速检查并监控 ScrollView 内视图…...
中药养发护发
按照中医理论,头发和肝肾有密切联系,肝主血,肾藏精, 其华在发,肝肾强健,上荣于头,则毛发乌黑浓密. 中药育发的应用 以当归,天麻,桑疹子养血润发,配合干姜祛风活血,能通畅经络, 加快循环,激活毛囊,能促进皮肤组织营养成分吸收和废弃物的排泄,改善 头发生态. 用苦参 皂角 清热化…...
Java面试题-集合类
目录 1、请简单介绍下 Java 的集合类吧。 Collection Set TreeSet和HashSet List ArrayList 和 LinkedList 数组和链表的区别 Java 的列表有哪些实现类? Vector Queue Map 能说下 HashMap 的实现原理吗? 能说下 HashMap 的扩容机制吗&#x…...
【Vue3】组件通信之v-model
【Vue3】组件通信之v-model 背景简介开发环境开发步骤及源码总结 背景 随着年龄的增长,很多曾经烂熟于心的技术原理已被岁月摩擦得愈发模糊起来,技术出身的人总是很难放下一些执念,遂将这些知识整理成文,以纪念曾经努力学习奋斗的…...
【Golang 面试 - 进阶题】每日 3 题(二)
✍个人博客:Pandaconda-CSDN博客 📣专栏地址:http://t.csdnimg.cn/UWz06 📚专栏简介:在这个专栏中,我将会分享 Golang 面试中常见的面试题给大家~ ❤️如果有收获的话,欢迎点赞👍收藏…...
Java中等题-多数元素2(力扣)【摩尔投票升级版】
给定一个大小为 n 的整数数组,找出其中所有出现超过 ⌊ n/3 ⌋ 次的元素。 示例 1: 输入:nums [3,2,3] 输出:[3] 示例 2: 输入:nums [1] 输出:[1]示例 3: 输入:num…...
100条超牛的DOS命令
目录 1. 文件和目录管理 1.1 列出文件和目录 1.1.1 dir 1.1.2 dir /w 1.2 切换目录 1.2.1 cd 1.2.2 cd .. 1.3 创建和删除目录 1.3.1 md / mkdir 1.3.2 rd / rmdir 1.4 文件操作 1.4.1 del / erase 1.4.2 copy 1.5 文件重命名 1.5.1 ren / rename 1.5.2 move …...
大数据信用报告查询会不会留下查询记录?怎么选择查询平台?
最近有不少网友都在咨询一个问题,那就是大数据信用报告查询会不会留下查询记录,会不会对自己的征信产生影响,下面本文就详细为大家介绍一下,希望对你了解大数据信用有帮助。 首先、大数据信用与人行征信是独立的 很多人只知道人行…...
JS【详解】内存泄漏(含泄漏场景、避免方案、检测方法),垃圾回收 GC (含引用计数、标记清除、标记整理、分代式垃圾回收)
内存泄漏 在执行一个长期运行的应用程序时,应用程序分配的内存没有被释放,导致可用内存逐渐减少,最终可能导致浏览器崩溃或者应用性能严重下降的情况,即 JS 内存泄漏 可能导致内存泄漏的场景 不断创建全局变量未及时清理的闭包&…...
第三期书生大模型实战营之Llamaindex RAG实践
基础任务 任务要求:基于 LlamaIndex 构建自己的 RAG 知识库,寻找一个问题 A 在使用 LlamaIndex 之前InternLM2-Chat-1.8B模型不会回答,借助 LlamaIndex 后 InternLM2-Chat-1.8B 模型具备回答 A 的能力,截图保存。 streamlit界面…...
【从0到1进阶Redis】Jedis 理解事务
笔记内容来自B站博主《遇见狂神说》:Redis视频链接 小伙伴们可以熟悉一下本专栏的 Redis 文章,可以更好地理解 正常操作 package oldfe.study;import com.alibaba.fastjson.JSONObject; import redis.clients.jedis.Jedis; import redis.clients.jedis.T…...
MySQL之Lost connection to MySQL server during query复现测试
测试Lost connection to MySQL server during query复现条件 环境报错信息复现测试方式一方式二 环境 Python: 3.8/3.9 Mysql: 5.x 报错信息 File "/Users/xxx/lib/python3.9/site-packages/sqlalchemy/dialects/mysql/base.py", line 2509, in do_rollbackdbapi_con…...
中国AI大模型场景探索及产业应用调研报告
AI大模型发展态势 定义 AI大模型是指在机器学习和深度学习领域中,采用大规模参数(至少在一亿个以上)的神经网络模型,AI大模型在训练过程中需要使用大量的算力和高质量的数据资源。 产业规模 2023年,中国大模型市场规模为147亿。结合《202…...
Linux--shell脚本语言—/—<1>
一、shell简介 Shell是一种程序设计语言。作为命令语言,它交互式解释和执行用户输入的命令或者自动地解释和执行预先设定好的一连串的命令;作为程序设计语言,它定义了各种变量和参数,并提供了许多在高级语言中才具有的控制结构&am…...
【java框架开发技术点】通过反射机制调用类中的私有或受保护的方法
示例 假设我们有一个类 ExampleClass,其中有一个私有方法 privateMethod: public class ExampleClass {private void privateMethod(String message) {System.out.println("Private method called with message: " + message);} }我们可以使用上述代码来调用这个…...
织梦模板网站源码下载/国际新闻稿件
2019独角兽企业重金招聘Python工程师标准>>> 整数集合 《Redis设计与实现》 整数集合(intset)是Redis中集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素不多时,Redis就会使用整数集合作为集合键的底层实现。…...
单页网站建设哪里有提供/南京百度网站快速优化
集群(cluster)与节点(node) 一个节点是一个集群的一部分。 索引(Index)、类型(Type)、文档(Document) 索引:含有相同属性的文档的集合类型:索引可以定义一个或多个类型,文…...
晋江外贸网站建设/有友情链接的网站
原标题:处理Java异常的9个最佳实践原创于【模棱博客】 http://www.flammulina.comJava中的异常处理不是一个简单的主题。初学者发现很难理解,甚至有经验的开发人员也可以花几个小时讨论如何以及应该抛出或处理哪些Java异常。这就是为什么大多数开发团队都…...
哪个素材网站做美工最好/百度24小时人工电话
处理器命名规则 显卡 核显 核心显卡 家用办公建议 可能是CPU上会自带显卡 显卡天梯图 为什么会有显卡天梯图 评分原因 具体看这个 https://www.zhihu.com/question/515018239/answer/2625409877...
西安个人做企业网站/推广策略包括哪些内容
这个方向与技术方案的实现,或产品的思考有很大关系的。 如,现在的某厂的【燃气报警器】,看上去不错吧。自动采集空气一氧化碳等危险气体的数据, 过界限了,就响,或关闭燃气开关。从功能上看,是…...
电商网站开发文字教程/排位及资讯
技能目标 掌握更多配置数据源的方法理解 Bean 的作用域会使用 Spring 自动装配会拆分 Spring 配置文件 本章任务 学习本章,读者需要完成以下 4 个任务。记录学习过程中遇到的问题,并通过 自己的努力或访问 kgc.cn 解决。 任务1:使用多种…...