写点什么

代码语言的魅力

作者:百度Geek说
  • 2022 年 5 月 18 日
  • 本文字数:4935 字

    阅读完需:约 16 分钟


本期技术加油站《代码语言的魅力》给大家带来 3 个部分的内容:浅谈 V8 Hidden Classes 和 Inline Caches;浅析 Java 逻辑运算与位运算;理解 Golang 的 type func(),希望能为大家的技术提升助力!

01 浅谈 V8 Hidden Classes 和 Inline Caches

Javascript 是动态的、基于属性链的语言,V8 是流行的 JavaScript 运行引擎。我们知道在运行时可以改变对象的属性和类型。为了定位对象的属性和类型,V8 引入隐藏类(Hidden Classes)概念,用于优化属性访问速度。看如下代码:


function Person(name, age) {  this.name = name;  this.age  = age;}
const zhangsan = new Person('Zhangsan', 20);const xiaofang = new Person('Xiaofang', 21);xiaofang.gender = 'female';
复制代码


初始化 Person 类时会给对象 zhangsan 创建隐藏类 C0,C0 还没有包含任何属性。当初始化属性 zhangsan.name 时会基于 C0 创建出新隐藏类 C1。由此类推,属性 age 初始化后,生成 C2。当初始化对象 xiaofang 时,由于属性结构相同其会复用 C0、C1、C2。但是给对象 xiaofang 增加属性 gender 时,其会基于 C2 生成新隐藏类 C3。


但如果你不小心执行了 delete 操作,那么情况就比较糟糕,如下:


function Person(name, age) {    this.name = name;    this.age  = age;}for (let i=0; i<1000000; i++) {    const xiaofang = new Person('Xiaofang', 21);    delete xiaofang.name;}
复制代码


上述代码加上 delete 语句,执行耗时 230ms。而去掉 delete 语句,执行耗时 4ms。差距如此大的原因就是在于隐藏类的破坏。我们可能一不小心在某些场景使用了 delete,特别是大循环体内时,性能退化就明显了。我们可以使用赋值 null 替换 delete。


V8 更进一步做了 Inline Caches 策略,可进一步提高属性访问速度。当多次访问属性时,如果能保持一个类的方法的参数类型不变,上下文不变,执行过程点不变,那么根据该缓存策略,函数内对象属性查找过程就能避免,提高函数执行速度。我们经常可能喜欢在函数体内定义函数,这些应该尽量避免。

02 浅析 Java 逻辑运算与位运算

逻辑运算与位运算


算数、赋值、逻辑、关系、自增自减、条件以及位运算等丰富的运算和相应的运算符是 Java 语言的主要特点之一,也是我们学习一门语言时的基础。本文我们主要简单了解一下 Java 中的逻辑运算和位运算,以及我们实际开发过程中如何更有效的应用它们的特性。


首先了解什么是逻辑运算和位运算:


逻辑运算:逻辑运算又称布尔运算。逻辑运算符要求操作数的数据类型为逻辑型,其运算结果也是逻辑型(boolean)值。逻辑运算符把各个运算的关系表达式连接起来组成一个复杂的逻辑表达式,以判断程序中的表达式是否成立,判断的结果是 true 或 false。


位运算:位运算是以二进制位为单位进行的运算,其操作数和运算结果都是整型值。这些整数类型包括 long,int,short,char 和 byte。


运算符


Java 中是如何执行这些运算的呢?那就是通过运算符。


逻辑运算符:短路与 "&&"、短路或 "||"、逻辑非 "!"、逻辑或 "|"、逻辑与 "&"。



  • &&(短路与)与 &(逻辑与)区别:看含义,区别就在于"短路",看说明"如果 a 为 false 则结果为 false,且不会计算 b"(因为不论 b 为何值,结果都为 false)。

  • ||(短路或)与 |(逻辑或)区别:同上区别同样在"短路"。a | b :如果 a 为 true 则结果为 true,但会继续计算 b(不具备短路能力时,会计算所有逻辑项)。


实践一

基于以上的学习,下面这个问题,在实际开发过程中你会如何决策呢?假设你有一个需求需要同时判断多个条件,这些条件你会有意识的做前后排序还是简单的无序罗列呢?如下你会选择方案 1 还是方案 2 呢?


public class Test {    public static void main(String[] args) {        // 方案1、a() && b()        System.out.println(a() && b());        // 方案2、b() && a()        System.out.println(b() && a());    }
/** * 一个耗时的逻辑判断 */ public static boolean a() { System.out.println("do a!"); boolean a = false; // 耗时耗性能的逻辑 for (int i = 0; i < 1000000; i++) { // 耗时操作 a = true; } return a; }
/** * 一个简单的逻辑判断 */ public static boolean b() { System.out.println("do b!"); return false; }}
复制代码


想必大家都能做出正确的选择,这其实是一个非常小的知识点,但平时开发过程很多人都会忽视。我们在日常开发过程中要注重细节,越是基础的逻辑越能积蓄更大的能量,养成良好的编程习惯以及性能意识。


短路与(&&)和短路或(||)由于短路机制的存在,能够优化逻辑运算的计算,从而提高效率。在实际开发过程中,应该有意识的使用短路与和短路或的短路能力,来优化我们的逻辑运算。



位运算符:包含位逻辑运算符(位与 '&',位或 '|',位非 '~',位异或 '^'),位移运算符(右移 '>>',左移 '<<',右移补零 '>>>')



逻辑比较简单,我们不再详述,我们主要看看位运算如何应用到我们的日常实际开发中。也许我们会发现,我们日常开发中好像并不经常用到位运算,但我们在看 Android 源码的时候确能经常看到相应的使用。比如 View 中的 mPrivateFlags、mPrivateFlags1、mPrivateFlags2、mPrivateFlags3 等等,它们一个变量甚至可以保存几十个不同的状态,这也就是通过位运算能够达到的一个优势,可以让“多状态”的管理像单状态管理一样简单高效。

实践二

假设一共有七种颜色,一个物体同时能拥有多种颜色,我们该如何简单高效的维护该物体的颜色属性呢?


1、维护一个颜色属性集合,每拥有一个颜色就向集合中添加,移除则删除?


2、维护 7 个布尔值指向不同的色值,每拥有一个就设置为 true,否则为 false?


以上只是简单的举几个例子,比如用布尔值来维护状态,这也是我们最常用的方法,这种对单状态的维护会很简单和直观,但对多状态的维护就会显得不那么适合。


这时我们就可以尝试考虑采用位运算的方式。回顾位运算的按位与和按位或,按位或在对应位不同时则为 1,是不是等同于我们添加了一个状态?同理,按位与在对应位都是 1 时则为 1,是不是就可以判断是否具有某个状态?我们则可用不同位的 1 来表示不同的状态,最终结果具有哪个位置的 1 就说明具有哪个状态,这样我们就能以一个属性来同时管理多个状态了。


public class Test {    /** 单属性维护多状态 */    private static int mColors = 0B00000000;    private static final int RED = 0B00000001;    private static final int ORANGE = 0B00000010;    private static final int YELLOW = 0B00000100;    private static final int GREEN = 0B00001000;    private static final int BLUE = 0B00010000;    private static final int PURPLE = 0B00100000;    private static final int PINK = 0B01000000;
public static void main(String[] args) { // 同时具有红绿蓝三色,mColors = 0B00011001 mColors = mColors | RED | GREEN | BLUE; System.out.println(mColors); // 判断是否具有红色,true System.out.println((mColors & RED) != 0); // 判断是否具有黄色,false System.out.println((mColors & YELLOW) != 0); // 添加黄色,mColors = 0B00011101 mColors |= YELLOW; // 判断是否具有黄色,true System.out.println((mColors & YELLOW) != 0); // 移除黄色,mColors = 0B00011001 mColors &= ~YELLOW; // 判断是否具有黄色,false System.out.println((mColors & YELLOW) != 0); }}
复制代码



总结



基础语法中存在容易让人忽视的特性,有时却会有很大的妙用,掌握这些特性,能够为我们解决实际问题提供更多的可能性,以及拓宽我们的视野。

03 理解 Golang 的 type func()



在 Go 语言中,可以用 type 关键字自定义类型,比如常用的:


type Myint int // 定义了MyInt类型,其基础类型是int,与int有相同的底层数据结构,但是是完全不通的两种类型type Book struct {  // 定义了一个Book结构体,包含Title和PageNum两个字段    Title    string    PageNum  int}
复制代码


同样地,Go 语言也支持自定义函数类型,具有相同的参数和返回值列表(不包括参数名与函数名,需要参数和返回值列表的类型与顺序一致)的函数被视为同一种类型。既然函数被当做一种类型可以被定义,那同样地,再 Go 语言里,函数可以像其他类型一样,被当做普通的值,再其他函数之间传递、作变量赋值、做类型判断和类型转换,举个例子:


type PowerCanculator func(num int) int   // 幂次计算 func PowerBase2(num int) int {    // 以2为底的幂次计算    return 1 << num} func main() {    var pc PowerCanculator    var result int    pc = PowerBase2    result = pc(2)    fmt.Println(result) // 结果为4}
复制代码


在以上例子里,PowerBase2 作为 PowerCanculator 类型的一种实现,被当做参数赋值给一个 PowerCanculator 类型的函数变量并调用。


那么函数被作为参数传递有什么好处或者妙用呢,我们可以考虑这样一个场景:我们希望有个计算器,能够进行任意数字计算,且所有参数和具体的操作都由调用方给出,这样我们的计算器仅做统一调度即可,该如何实现?


type PowerCanculator func(num int) int   // 幂次计算  func PowerBase2(num int) int {    // 以2为底的幂次计算    return 1 << num} func PowerBase4(num int) int {    // 以4为底的幂次计算    return 1 << (num+1)} func Canculator(num int, pc PowerCanculator) (int, error) {    if pc == nil {        return 0,errors.New("param error")    }    return pc(num),nil} func main() {   result1, err := Canculator(2, PowerBase2)   if err != nil {      fmt.Println(err)   }   fmt.Println(result1) //结果为4   result2, err := Canculator(2, PowerBase4)   if err != nil {      fmt.Println(err)   }   fmt.Println(result2) //结果为16}
复制代码


以上是一个函数被当做另外一个函数的参数传入的例子,那么函数既然被当做了一个普通参数,那么它是否也可以被当做另外一个函数的返回值返回呢,答案是可以:


fun PowerCanculatorGenerator(base int)  PowerCanculator {    switch base{    case 2:        return PowerBase2    case 4:        return PowerBase4    default:        return nil    }}
func main() { var base int var powerBase2 PowerCanculator var powerBase4 PowerCanculator powerBase2 = PowerCanculatorGenerator(2) powerBase4 = PowerCanculatorGenerator(4) fmt.Println(powerBase2(2)) //结果为4 fmt.Println(powerBase4(2)) //结果为16}
复制代码


以上是一个简单的例子,其实函数类型最典型的应用场景,是进行 http 服务路由注册的时候,比如说现在有两个 http 请求 uri 分别是/sayhi 和/saybye,在进行 http 请求实现时,可以这么写:


func mHttp() {    http.HandleFunc("/sayhi", hi)       http.HandleFunc("/sayBye", bye)    http.ListenAndServe("0.0.0.0:8888",nil)} func hi(w http.ResponseWriter, r *http.Request) { } func bye(w http.ResponseWriter, r *http.Request) { }
复制代码


其中在 http 包里就存在一个函数类型:


type HandlerFunc func(ResponseWriter, *Request)
复制代码


不难看出,HandlerFunc 是一个函数类型,有两个参数,而 hi 和 bye 是对 HandlerFunc 的实现,通过指定 HandleFunc 里函数变量的值,实现不同服务的路由注册。


以上内容,其实总得来说是在说明函数的两个关键特性:


1.可以被当做参数传入函数;


2.可以作为返回值从函数中返回。


其实,函数在 Go 语言中被称作『一等公民』,除了拥有函数的基本特性和上述两个特性之外,还具备更多特殊运用,比如对函数进行显式类型转换等,这里就不多做赘述,感兴趣的同学可以自行查阅相关文章和书籍~


----------  END  ----------


推荐阅读【技术加油站】系列:


百度程序员Android开发小技巧


Chrome Devtools调试小技巧


人工智能超大规模预训练模型浅谈

用户头像

百度Geek说

关注

百度官方技术账号 2021.01.22 加入

关注我们,带你了解更多百度技术干货。

评论

发布
暂无评论
代码语言的魅力_百度Geek说_InfoQ写作社区