现代编程语言那些让人激动的特性
引言
有没有觉得,发展到现在,软件开发行业是越来越成熟了,无论是过程管理、架构方法、设计方法,还是语言、平台、框架、工具等,都发展到了一个前所未有的高度,相关思想和理念也日臻完善,我们真正进入了一个最好的时代。
单就编程语言来说,近些年包括Scala(2003)、Groovy(2003)、Go(2009)、Kotlin(2011)、Swift(2014)等新兴编程语言如雨后春笋版涌现出来,也给我们带来了很多让人眼前一亮的编程特性,甚至Java这等老牌编程语言也是不断推陈出新,编程再也不像过去那般枯燥。
本篇就带大家一起感受一下现代编程语言那些激动人心的特性。原型(Prototype)
这个特性其实有点早了,但是也是很早就让人感动的语言特性了,熟悉Javascript的同学应该对它很了解。Javascript语言具有动态性,我们可以随时为类的某个实例添加方法,也可以利用动态原型,为类的所有实例添加方法,有没有感觉扩展类的实现变得非常方便了呢?//声明一个Person类 class Person { constructor(name, birthdate) { this.name = name; this.birthdate = birthdate; } } //实例化两个Person对象 let person1 = new Person("张三", Date.now()); let person2 = new Person("李四", Date.now()); //为其中一个person对象动态添加名为debug的方法,并调用 person1.debug = function() { console.debug(this); } person1.debug(); //利用动态原型为Person类添加名为debug的方法 Person.prototype.debug = function() { console.debug(this); } //实例化一个新的Person对象,并调用其debug方法 let person3 = new Person("王五", Date.now()); person3.debug();扩展(Extension)
扩展和原型很像,允许我们在不修改或继承类的情况下,将新的函数方法添加到原类中。这个特性较早见于C#这门语言,目前在Kotlin、Swift中均可以看到。这里顺便说一下C#,当时C#出来的时候,不得不说很多特性是非常棒的,包括扩展方法、泛型、分部类等等,比Java好不要太多。像Kotlin,不仅可以扩展类的方法,还可以扩展类的属性。// Java中,通过继承String类来添加capitalize方法,实现首字母大写 public class StringCapitalize extends String { public String capitalize() { var character = this.substring(0, 1).toUpperCase(); var rest = this.substring(1, this.length() - 1).toLowerCase(); return character + rest; } } // Java中,通过添加工具类扩展String类 public class StringUtils { public static String capitalize(String string) { var character = string.substring(0, 1).toUpperCase(); var rest = string.substring(1, string.length() - 1).toLowerCase(); return character + rest; } } // Kotlin中,通过Extension特性,为String类添加方法。作用域内所有的String实例均有了这个方法。 fun String.capitalize(): String { val character = substring(0, 1).toUpperCase() val rest = substring(1, length - 1).toLowerCase() return character + rest }隐式接口实现
前两个都是关于扩展代码的,这里再来一个。我们知道Java 1.8以来,接口interface里的方法可以有自己的默认实现了,大大方便了实现类,减少了重复代码。相对于Java的这个实现是显示的,Go语言的接口实现可以是隐式的,添加隐式实现后,所有继承的结构(Go没有类,都是结构struct)都可以调用这个方法,和前面的两个特性有异曲同工之妙,下面我们对比看一下。// 这是Java的接口,里面默认显示实现了display方法 public interface Shape { float area(); float perimeter(); default void display() { System.out.println(area()); } } // 这个类实现了上面的接口,其实例对象可以直接调用display方法 public class Rectangle implements Shape { public final float width; public final float height; public Rectangle(float width, float height) { this.width = width; this.height = height; } //.....中间省略若干代码... public static void main(String... args) { var rect = new Rectangle(2.0f, 3.0f); rect.display(); //调用接口中的默认实现 } } // ------------------以下为 Go语言实现 ----------------- package main import ( "fmt" ) // 接口定义 type shape interface { area() float32 perimeter() float32 } // 结构定义 type rectangle struct { width float32 height float32 } // 添加area、perimeter函数到rectangle struct func (rect rectangle) area() float32 { return rect.width * rect.height } func (rect rectangle) perimeter() float32 { return 2 * rect.width + 2 * rect.height } // 为shape接口隐式添加display方法及实现 func display(shape shape) { fmt.Println(shape.area()) } // 为实例调用display方法 func main() { rect := rectangle{width: 2, height: 3} display(rect) }宏(Macro)
C语言就有宏的概念,通过 #define 定义,然后在代码中进行替换。宏作为Rust语言的高级特性,可以操作语法单元,是一种通过编写代码来生成代码的方式,被称作"元编程"(meta programming)。相对于函数,宏可以接受任意多个参数,可以减少重复代码,定义DSL。宏语法比较复杂,难以编写和调试,以至于在Rust文档中说,宏将是其最后的特性。// 定义一个生成函数的宏,将通过模式匹配生成函数 macro_rules! create_function { ($func_name:ident) => ( fn $func_name() { println!("function {:?} is called", stringify!($func_name)) } ) } fn main() { // 调用之前定义的宏 create_function!(foo); // 也可以这样:create_function![foo]; foo(); } // 定义名为vector的宏,会根据第18行模式匹配的次数,重复第21行语句相同次数 macro_rules! vector { ($($x:expr),*) => { { let mut temp_vec = Vec::new(); $(temp_vec.push($x);)* temp_vec } }; } // 调用上面的宏生成的函数,创建一个Vector fn main() { let a = vector![1, 2, 4, 8]; println!("{:?}", a); } // 相当于执行了如下代码 { let mut temp_vec = Vec::new(); temp_vec.push(1); temp_vec.push(2); temp_vec.push(4); temp_vec.push(8); temp_vec }自动属性
当你回想写代码枯燥的时候,应该会想到为字段编写getter、setter吧?较早的时候,C#就意识到了这个问题,贴心地推出了自动属性这个语法糖。而Java开发者则是通过Eclipse、IDEA这样的开发工具来自动生成getter、setter代码。当然,现在也可以依赖Lombook包,使用lombok的注解@Getter @Setter来编译时生成相关代码。// Java:需要手动编写getter、setter public class Person { private String name; public setName(String name) { this.name = name; } public getName() { return this.name; } } // C#:有了自动属性,是不是省事多了? public class Person { public string Name { get; private set; } }可选链(optional chaining)
据说空指针异常是软件业最贵的异常,价值10亿美元。你有没有为处理调用链中的null值而烦恼过?又或者被伤害过?Kotlin会在编译期提示对可能为null变量的不安全使用,也提供了Elvis 操作符 ?: 来方便地处理null值。而有了可选链,就舒服多了。可选链语法应该较早出现在JavaScript语言中,新兴语言Swift也提供了这一省心的特性。Swift英明地决定变量是不允许直接存储NIL值,当然也提供了optionals的装箱功能允许将NIL或其它值包装起来,方便有时使用。// --------------------这个是JavaScript里的示例-------------------------- const obj = { a: { b: [{ name: "obj" }] } } // 原本的写法 console.log(obj && obj.a && obj.a.b.length && obj.a.b[0].name) // 可选链写法,是不是很酷? console.log(obj?.a?.b?.[0]?.name); // obj console.log(obj?.b?.c?.d) // undefined // --------------------这个是Swift里的示例-------------------------- class Person { var residence: Residence? //这里是一个optional的语法,该字段可存储NIL值 } class Residence { var numberOfRooms = 1 } let john = Person() // 链接可选residence?属性,如果residence存在则取回numberOfRooms的值 if let roomCount = john.residence?.numberOfRooms { print("John 的房间号为 (roomCount)。") } else { print("不能查看房间号") }卫语句(Guard)
输入乃万恶之源,函数首要的事情就是检查不规范和不安全的输入,这也是卫语句的来历。Swift语言为此提供了专门的卫语句语法,有了它的贴身防护,整个代码都干爽多了,剧烈运动都不怕,不信往下瞧:// 异常枚举 enum CircleAreaCalculationError: ErrorType { case RadiusNotSpecified case RadiusUnvalid } // 传统的卫语句写法(当然可以写的更简洁,这个写法明显是故意的) func calculateCirlceArea(radius:Double?) throws -> Double { if let radius = radius { if radius > 0 { return radius*radius*M_PI } else { throw CircleAreaCalculationError.RadiusUnvalid } } else { throw CircleAreaCalculationError.RadiusNotSpecified } } // 使用guard关键字的写法,代码不再嵌套那么多了吧 func calculateCirleArea(radius:Double?) throws -> Double { guard let radius = radius else { throw CircleAreaCalculationError.RadiusNotSpecified } guard radius > 0 else { throw CircleAreaCalculationError.RadiusUnvalid } return radius*radius*M_PI }Lambda表达式
如果要评选最酷的语言特性,那么Lambda表达式必须获得提名。Lambda表达式很早就出现在Lisp语言中,python也有,在后来的C#语言大放异彩,又一次狠狠地羞辱了不长进的Java,而Java也终于在1.8版本后加入了这一特性,甚至C++ 11也光荣地上车了。// JDK7 匿名内部类写法 List list = Arrays.asList("I", "love", "you", "too"); Collections.sort(list, new Comparator(){// 接口名 @Override public int compare(String s1, String s2){// 方法名 if(s1 == null) return -1; if(s2 == null) return 1; return s1.length()-s2.length(); } }); // JDK8 Lambda表达式写法 List list = Arrays.asList("I", "love", "you", "too"); Collections.sort(list, (s1, s2) ->{// <-----注意这里!!!!,是不是很酷? if(s1 == null) return -1; if(s2 == null) return 1; return s1.length()-s2.length(); }); // JDK8 Lambda表达式写法 new Thread( () -> System.out.println("Thread run()") // <-------还有这里!!!帅不帅? ).start(); // python 中的lambda def make_incrementor(n): return lambda x: x + n addFive = make_incrementor(5) addFive(10) # 返回15,动态生成函数,腻害不腻害?渐进式类型(Gradual Typing)
我们知道编程语言有静态和动态之分,静态语言如Java 、 C# 、 C 和 C++,动态语言如Perl,Python,JavaScript,Ruby 和 PHP等,多数为脚本语言。而融合了静态和动态特性的语音,就被称为渐进式语言,如TypeScript、Common LISP、Dylan、Cecil、Visual Basic.NET、Bigloo Scheme、Strongtalk等。静态类型检查可以尽早地发现 BUG,动态类型检查可以方便地处理依赖于运行时信息的值的类型。渐进式语言允许类型注释来控制程序的一部分使用静态类型检查,而另一部分为动态检查,更具灵活性。 Python从3.5开始引入了对静态类型检查的支持。# 静态检查,已注明参数类型为float def pide(pidend: float, pisor: float) -> float: return pidend / pisor # 动态检查,未注明参数类型 def pide(pidend, pisor): return pidend / pisor不变性(Immutability)
在面向对象的编程语言中,状态是计算的基础。由于可变状态的存在,在编写高并发,多线程代码时,无法知道并行进行的诸多状态读写中是否有顺序上的错误,而且这种错误又是难以察觉的,而不变性则规避了这个问题。不变性是函数式编程的基础,不变性意味着函数没有副作用,无论多少次执行,相同的输入就意味着相同的输出,所有线程都可以无所顾忌的执行同一个函数的代码,代码更像数学函数,更易理解和测试。
面向对象的编程通过封装可变动的部分来构造出可让人读懂的代码,函数式编程则是通过最小化可变动的部分来构造出可让人读懂的代码。
String就是构建在Java语言内核中的不可变类的一个典型例子。Java 的 CopyOnWrite系列容器类也是利用了不变性增强了并发安全性。Java可以通过final修饰符实现类和变量的不可变。而Scala、Swift、Groovy等语言也有各自的语法实现不可变的变量和类。//Java、Groovy的不变量 private final String immutable; //Scala的不变量,注意val,而不是var val immutable = 1 // Swift使用let声明不变量,而不是var let immutable 1 // Groovy使用注解声明不可变类型 @Immutable class Preson { } // Swift实现不可变类 struct Person { let name:String let interests:[String] }多重分派(Multiple Dispatch)
多重分派是一些编程语言的特性,其中的函数或者方法,可以在运行时间(动态的)使用一个或多个实际参数的组合特征,路由动态分派至实现函数或方法。多重分派主要区别于我们常见的重载方法,重载方法是在编译期就绑定了,而多重分派是在运行期分派的。Lisp、Julia、C#、Groovy等语言内建多分派特性,JavaScript、Python和C等语言通过扩展支持多分派。多重分派可以避免我们写很多分支条件,而是更直观地用对象类型表达,使代码变得可读性更好并且较少发生错误。 //Julia多分派示例 collide_with(x::Asteroid, y::Asteroid) = ... # deal with asteroid hitting asteroid collide_with(x::Asteroid, y::Spaceship) = ... # deal with asteroid hitting spaceship collide_with(x::Spaceship, y::Asteroid) = ... # deal with spaceship hitting asteroid collide_with(x::Spaceship, y::Spaceship) = ... # deal with spaceship hitting spaceship // 以下是c#多分派示例(注意关键字dynamic) class Program { static void Main() { Console.WriteLine(Collider.Collide(new Asteroid(101), new Spaceship(300))); Console.WriteLine(Collider.Collide(new Asteroid(10), new Spaceship(10))); Console.WriteLine(Collider.Collide(new Spaceship(101), new Spaceship(10))); } } static class Collider { public static string Collide(SpaceObject x, SpaceObject y) => ((x.Size > 100) && (y.Size > 100)) ? "Big boom!" : CollideWith(x as dynamic, y as dynamic); private static string CollideWith(Asteroid x, Asteroid y) => "a/a"; private static string CollideWith(Asteroid x, Spaceship y) => "a/s"; private static string CollideWith(Spaceship x, Asteroid y) => "s/a"; private static string CollideWith(Spaceship x, Spaceship y) => "s/s"; } abstract class SpaceObject { public SpaceObject(int size) => Size = size; public int Size { get; } } class Asteroid : SpaceObject { public Asteroid(int size) : base(size) { } } class Spaceship : SpaceObject { public Spaceship(int size) : base(size) { } } // 以下是python用多分派实现的剪刀、石头、布 from multipledispatch import dispatch @dispatch(Rock, Rock) def beats3(x, y): return None @dispatch(Rock, Paper) def beats3(x, y): return y @dispatch(Rock, Scissors) def beats3(x, y): return x @dispatch(Paper, Rock) def beats3(x, y): return x @dispatch(Paper, Paper) def beats3(x, y): return None @dispatch(Paper, Scissors) def beats3(x, y): return x @dispatch(Scissors, Rock) def beats3(x, y): return y @dispatch(Scissors, Paper) def beats3(x, y): return x @dispatch(Scissors, Scissors) def beats3(x, y): return None @dispatch(object, object) def beats3(x, y): if not isinstance(x, (Rock, Paper, Scissors)): raise TypeError("Unknown first thing") else: raise TypeError("Unknown second thing") 解构(Destructuring)
前面几个特性是不是略显沉闷,那么来看一下这个激动一下。解构这一语法特性用于从数组索引或对象属性创建变量,简直帅到飞起。// JavaScript ES6 的解构示例 var [ foo ] = array; var foo = array[someIndex]; // 还是JavaScript的,有没有受惊??? [a, b, ...rest] = [10, 20, 30, 40, 50]; console.log(rest); // 将输出: Array [30,40,50] // 以下3个是Python的解构 start, *_ = [1, 4, 3, 8] print(start) # 输出 1 print(_) # 输出 [4, 3, 8] *_, end = ["red", "blue", "green"] print(end) # 输出 "green" start, *_, (last_word_first_letter, *_) = ["Hi", "How", "are", "you?"] print(last_word_first_letter) # 输出 "y"内联测试(Inline Testing)
爱写单元测试的同学有福了,这个绝壁是重磅炸弹,在生产代码里夹着测试代码,你有想过这么写测试吗?谁想的?简直脑洞打开啊!该特性在Pyret语言中,Pyret旨在作为编程教育的杰出选择,同时探索脚本和函数式编程的融合。// Pyret fun sum(l): cases (List) l: | empty => 0 | link(first, rest) => first + sum(rest) end where: //这里就是测试代码啦,简直要疯了 sum([list: ]) is 0 sum([list: 1, 2, 3]) is 6 end //复杂一点的 eps = 0.001 fun d-dx(f): doc: "Approximate the derivative of f" lam(x): (f(x + eps) - f(x)) / eps end where: fun square(x): x * x end fun around(delta, target): lam(actual): num-abs(actual - target) < delta end end dsquare = d-dx(square) dsquare(5) satisfies around(0.1, 10) dsquare(10) satisfies around(0.1, 20) end内联编译期(Inline Assemblers)
如果内联测试没有让你震惊,D语言内联编译期的这个特性绝对会让你惊掉下巴,基于该特性,开发人员可以直接在D语言中嵌入汇编代码,彻底放飞自我了,俺滴亲娘啊!受不了!受不了!顺便说一下,D语言比较小众,是C++的一个改进型,它包括了按合约设计、垃圾回收、关联数组、数组切片和惰性求值等特性。void *pc; asm { pop EBX ; mov pc[EBP], EBX ; }模式匹配(pattern matching)
好吧,我们看点其它的来压压惊吧。尽管Kotlin语言也说自己实现了模式匹配,但是实际上只是一点点帅,真正帅的是Elixir语言的模式匹配,Elixir作为一种在Erlang OTP上运行的动态类型语言,将模式匹配提升到了一个全新的水平。 // Kotlin所谓的模式匹配,虽然很帅,但是还有switch的影子 var statusCode: Int val errorMessage = when(statusCode) { 401 -> "Unauthorized" 403 -> "Forbidden" 500 -> "Internal Server Error" else -> "Unrecognized Status Code" } val errorMessage = when { statusCode == 401 -> "Unauthorized" statusCode == 403 -> "Forbidden" statusCode - 400 < 100 -> "Client Error" statusCode == 500 -> "Internal Server Error" statusCode - 500 < 100 -> "Server Error" else -> "Unrecognized Status Code" } // Elixir 的模式匹配,实现变量的解构,a、b、c将分别依次被赋值 {a, b, c} = {:hello, "world", 42} // head 将等于1, tail将等于[2, 3] [head | tail] = [1, 2, 3] (声明性循环)for-comprehensions
在编程语法上,Python真是个神一样的存在,for循环都能写出花来。// Python第一种写法 numbers = [] for n in [1, 2, 3, 4, 5]: numbers.append(n) print(numbers) //Python 的 for-comprehensions 写法 numbers = [n for n in [1, 2, 3, 4, 5]] print(numbers) # 输出[1, 2, 3, 4, 5] // 还可以计算 numbers = [n ** 2 for n in [1, 2, 3, 4, 5]] print(numbers) #输出 [1, 4, 9, 16, 25] // 还能过滤 numbers = [n ** 2 for n in [1, 2, 3, 4, 5] if n % 2 == 0] print(numbers) # 输出[4, 16] // 还能组合 numbers = [a:n for n in [1, 2, 3] for a in ["a", "b"]] print(numbers) # 输出[("a", 1), ("b", 1), ("a", 2), ("b", 2), ("a", 3), ("b", 3)] // 这不,把Scala也带坏了 val executionTimeList = List(("test 1", 100), ("test 2", 230)) val numberOfAssertsWithExecutionTime: List[(String, Int, Int)] = for { result <- results (id, time) <- executionTimeList if result.id == id } yield ((id, result.totalAsserts, time)) // numberOfAssertsWithExecutionTime 为 List[("test 1", 10, 100), ("test 2", 6, 230)] 流(Stream)
Java 8 中提供了Stream API特性, Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式,来提供一种对 Java 集合运算和表达的高阶抽象。事实上这个特性C#早就有了(Java又躺枪一次)。不得不说,利用这个特性写出来的代码,看上去还真的是很流利的。// Java 的 Stream API 语法 List transactionsIds = widgets.stream() .filter(b -> b.getColor() == RED) .sorted((x,y) -> x.getWeight() - y.getWeight()) .mapToInt(Widget::getWeight) .sum(); // c#的LINQ示例 int[] numbers = new int[7] { 0, 1, 2, 3, 4, 5, 6 }; var numQuery = from num in numbers where (num % 2) == 0 select num; 结语
至此,本篇就结束了,基本上领略了一下各编程语言的风情,有没有感觉一下子学会了很多编程语言?好的,以后关注到新的特性,还会再补充。