介绍“超级语言”——我为什么讨厌Java(1)

什么是超级语言

超级语言是一种说法,描述的是一种十分好用的语言,这种语言的功能丰富、开发效率高、在使用这种语言开发程序的时候使用者能赏心悦目。

试着对比以下不同语言实现相同功能的代码:

class Point
{
    public:
    int x;
    int y;
    std::string tag;
    Point(int xVal, int yVal, const std::string & tagVal)
    {
        x = xVal;
        y = yVal;
        tag = std::string(tagVal);
    }
}
class Point
{
    public int x;
    public int y;
    public String tag;
    public Point(int xVal,int yVal)
    {
        x = xVal;
        y = yVal;
    }
}
class Point:
    x = 0
    y = 0
    tag = string.empty
    def __init__(self, x, y, tag):
		self.x = x
		self.y = y
		self.tag = tag
class point
    {
        x: number;
        y: number;
        tag: string;
        constructor(xVAl: number,yVal: number,tagVal: string)
        {
            this.x = xVal;
            this.y = yVal;
            this.tag = tagVal;
        }
    }
public record Point(int X,int Y,string Tag);

是的,你没有看错,最后一种语言仅仅使用一行就实现了别的语言用十几行才能完成的功能。没错,最后一种语言就是我们今天要介绍的“超级语言”。

超级语言?

在正式开始前,我们不妨先用超级语言与Java进行一些对比。没错,还是上代码。

属性

如果这次的类,要求隐藏内部实现,所有的字段必须private,必须通过间接的方法进行读写,那么写法就应该是:

class Point
{
    private int x;
    private int y;
    private String tag;
    
    public int getX() { return x; }
    
    public int getY() { return y; }
    
    public String getTag() { return tag; }
    
    public void setX(int value) { x = value; }
    
    public void setY(int value) { y = value; }
    
    public void setTag(String value) { tag = value; }
    
    public Point(int xVal,int yVal)
    {
        x = xVal;
        y = yVal;
    }
}

再看看超级语言:

//此种方法创造的类与data class相同,这里是为了介绍属性才这么写
class Point
{
    public int X { get; set; }
    
    public int Y { get; set; }
    
    public string Tag  { get; set; }
}

这就是超级语言最好的一个地方——属性,有了属性这个东西,很容易将class写为immutable的。而且还可以非常简单地封装私有字段,既然简单,为什么要选择简单不了多少但是比较危险的public字段呢?

而且,属性还可以写成函数的形式,比如有一个属性,修改数值的时候还要进行别的操作:

private int _x;
public int X
{
    get => _x;
    set
    {
        _x = value;
        //Do something
    }
}

这种写法远比手写getXXXsetXXX要方便、简洁、明了。

属性带来的另一个好处就是索引器,比如访问JavaArrayList中的元素,一般是:

List<Integer> list = Lists.newArrayList();
//Do something to fill the list.
int val = list.get(15);

而超级语言则更为简单

var list = new List<int>();
//Do something to fill the list.
var val = list[15];

实际上,超级语言与C++不同,[]运算符并不允许重载,上面的访问方法就是this[]的属性。

这样的效果看起来差不了多少,仅仅是少打了4个字母而已。实际上,这种类似于数组的访问方式,只要你学过用[]访问数组的语言,都会倍感亲切,并且立马能知道这个是干什么的,看起来非常简洁、直观。

除此之外,拥有属性的超级语言在序列化Json的时候,一切都可以自动执行。而Java则必须使用public的字段才能做到如此的自动化,不方便程度立马就显现出来了。

out

我们进一步思考,如果我们用[]来访问一个字典——打住,为了防止异常,一般都不这么访问。

Java中,为了安全地获取Map中的值,一般都使用以下的方法:

Map<String, Integer> map = Maps.newHashMap();
//Do something to fill the map.
Optional<Integer> val = Optional.ofNullable(map.get("index"));
if(val.isPresent()){
    //Do something
}

或者喜欢null的可以这么写:

Map<String, Integer> map = Maps.newHashMap();
//Do something to fill the map.
Integer val = map.get("index");
if(val != null){
    //Do something
}

而在超级语言中,因为有out的存在,一般来说简单有方便的写法是

var dict = new Dictionary<string, int>();
//Do something to fill the dictionary.
if(dict.TryGetValue("index", out var value))
{
    //Do something
}

一个简单的TryGetValue方法,配合out,就能用一行代码,就能判断是否存在这个键,并且获得键对应的值。

但是这并不是out最常用的用法,如果要将一个字符串转化为整形,在Java中需要:

try{
	int val = Integer.parseInt(str);
    //Do something.
} catch (NumberFormatException ex){
    //Handle the exception.
}

由于加了trycatch的块,整个代码显得十分臃肿,而在超级语言中,配合out的方法,依然可以又安全又方便地完成相同的工作:

if(!int.TryParse(str, out var val))
{
    //Handle non-int condition.
}
//Do something.

使用这种写法能够让代码更加简洁明了。

元组

使用out其实算是使用多个返回值的函数,那么如何在Java中返回多个返回值呢?一种简单是实现方法是使用Pair,比如:

Pair<Integer, Integer> Foo(){
    //Do something
    return Pair.of(val1, val2);
}

这看起来还比较友善,但关键是Java并不像C++一样自带pair的类型,你要使用,要么自己做一个,要么导入别的包,比如这个Pairapache.commons里的。

而且如果你要三个、四个、五个,甚至更多,都要找到适合的类,或者自己手动写这么多类,这就造成了很大的麻烦。

而超级语言,除了自带的Tuple类之外,还有元组可以使用:

(int, int) Foo()
{
    //Do something
    return (val1, val2);
}

在使用的时候也非常简单:

var (val1, val2) = Foo();
var val = Foo();

两种方式都可以,第一种直观,第二种节省键盘(用的时候就不一定节省了)。

如果要忽略某个值,还可以:

(_, val) = Foo();

还有一使用元组的小技巧奇淫技巧就是使用元组交换两个变量:

var str1 = "Super Laguage";
var str2 = "Junk Laguage Sucks";
(str1, str2) = (str2 ,str1);

这样的效果是与三变量法是相同的,而且省去了一个中间变量和两行代码。

除了这种情况,个人发掘元组使用的最佳场景是搜索,比如搜索的时候,往队列或者栈里直接插一个元组进去,省去了很多代码。

高性能

据我所知,也查找了互联网,如果有错误请指出,Java并没有方法在栈上申请一块内存空间进行使用。

在栈上申请内存空间,与在堆里申请一块内存空间(还要考虑GC的时间),时间差的是很多的。如果我稍微用的一个小东西,比如Pair,或者读取一个不大的小数组进行排序,这些都只能申请堆内存,导致性能十分低下。

而超级语言,有ref structstackalloc的加持,分配和使用栈上的内存就十分方便。

试想下面的场景,在一个游戏中,需要计算两个点之间的距离,在超级语言中,可以有以下的写法:

public struct Point
{
    public double X { get; set; }
    public double Y { get; set; }
    public double Z { get; set; }
}

class SampleGame
{
    double Distance(Span<Point> points)
    {
        if (points.Length < 2)
            return -1;
        return Math.Sqrt(Math.Pow(points[0].X - points[1].X, 2) + 
                         Math.Pow(points[0].Y - points[1].Y, 2) +
                         Math.Pow(points[0].Z - points[1].Z, 2));
    }
    
    void Foo()
    {
        Span<Point> points = stackalloc Point[2];
        //Do something to fill the span.
        var ans = Distance(points);
    }
}

其中,Span<T>就是一个ref struct,其中储存的内容可以在堆里,也可以在栈上。但是由于其有在栈上的可能性,因此并不能跨线程使用。在超级语言中struct是值类型,意味着每一次函数调用传递都会将值复制给被调用的函数,对其进行调用或更改不会改变原有的值。class是引用类型,每一次函数调用都会将引用传递给被调用的函数,对其进行操作会影响到原来的对象。而ref struct则是按引用传递的struct,因此在Foo调用Distance的时候不会发生拷贝,传递的Span中含有个指针,指向的内存是在栈中的两个Point

还有一种方法就是申请一块区域作为临时要用的缓冲区,常用于从流中读取内容,比如下面就是官方示例:

const int MaxStackLimit = 1024;
Span<byte> buffer = inputLength <= MaxStackLimit ? stackalloc byte[inputLength] : new byte[inputLength];
//Do something to use the buffer.

如果在Java中,只能申请堆中的内存作为缓冲区,因此会造成性能低下。

LINQ

还是给定上面的Point,如果要求从里面选出所有X大于10的Point,应该怎么做?在Java中,一般的写法是:

Point[] points = new Point[256];
//Do something to fill points.
List<Point> selected = newArrayList();
for(Point point: points){
	if(point.getX() > 10)
		selected.add(point);
}

还有一种使用collect写法,不过不够直观:

Point[] points = new Point[256];
//Do something to fill points.
List<Point> selected = Arrays.stream(points).filter(point -> point.getX() > 10)
    .collect(Collectors.toList());

超级语言也有类似的形式,比如:

var points = new Point[256];
//Do something to fill points.
var selected = points.Where(point => point.X > 10);

或许可能还是不大直观,还可以这么写:

var points = new Point[256];
//Do something to fill points.
var selected = from point in points where point.X > 10 select point;

第二种写法看起来与SQL十分相似,这种写法就叫做LINQ,即语言集成查询,只要你学过英语,或者会一些SQL,超级语言的第二种写法在你没学过超级语言的时候都能看懂。第一种写法也属于LINQ,属于LINQ Method Chain。而Javacollect方法看起来十分晦涩难懂,需要一些学习才能使用。

然而超级语言的LINQ被用在了更有意义的地方,即Entity Framework,使用LINQ的语法,很容易就能操作数据库,再也不需要手写SQL。比如官方文档中有这样的示例:

using (var db = new BloggingContext())
{
    var blogs = db.Blogs
        .Where(b => b.Rating > 3)
        .OrderBy(b => b.Url)
        .ToList();
}

这也可以写成下面的,看起来更直观:

using (var db = new BloggingContext())
{
    var blogs = from blog in db.Blog
        where blog.Rating > 3
        orderby blog.Url
        select blog;
}

两种写法各有优缺点,但是编译出来的内容是相同的,可以随个人喜好使用。

Java是无法拓展出这种用法的。

方便的协程

Java中,异步以及多线程是极其痛苦的事情,要手动创建线程池,写上一大堆代码。甚至官方库都没有实现,要写的简单点还要使用第三方库。

然而超级语言使用就十分简单:

public static async Task<int> ASampleAsync()
{
    await Task.Delay(100);
    //Do some time cost task.
    return 10;
}

public static async Task Main()
{
    var val = ASampleAsync();
    //Do other time cost task.
    var result = await val;
    //Should return 10
}

关键是,超级语言的很多官方库都具有异步的版本,这样就不需要自己去手动对耗时的工作进行封装。

有这样方便的协程,在编写GUI程序的时候不会造成主线程阻塞导致的UI卡顿,也极大地改善了作为Web服务器时的性能。

asyncawait 协程这里就不多说了,因为很多别的语言也有(为什么Java没有呢),连C++ 20都将加入。

这里再说一种异步,看下面计算斐波那契数列的代码:

public static IEnumerable<int> FibSequence(int max)
{
	var previousLast = 1;
	var last = 1;
	yield return 1;
	yield return 1;
	for (var i = 2; i < max; ++i)
	{
		(last, previousLast) = (previousLast + last, last);
		yield return last;
	}
}

这样每个元素之间返回的时间都是一个常数,而且返回的IEnumerable并不是一次性的,可以多次使用。

隐式变量声明与动态类型

在上面的所有超级语言的代码中,很多地方都用了var来声明变量,这是否说明了超级语言不是一种静态类型语言呢?

这么说对,但是也不对,超级语言也有动态类型,常用于序列化Json而不创建新的类型。

var name = "HCG";
var age = 10;
var json = JsonConvert.SerializeObject(new { Name = name, Age = age });

其中new {}就创建了一个动态类型。

其实var是超级语言的一个特性,这样在声明变量的时候不用将这个变量的名字写出来,这种特性在减少代码长度方面是立竿见影的,毕竟很多类型的名字都非常长,再加上泛型,就有可能是套娃接套娃了。无论类型多么长,三个英文字母就能将其表示,真的是十分方便。而且在你不确定一个函数的返回值的时候,使用var是绝对没错的,不然你就需要去查文档来确定返回值的类型。当然,IDE这里可是帮不了你,毕竟在你打出函数名称的一部分前,IDE是没法自动补全函数名称以及返回值类型的。

这两个十分方便的特性,Java都没有。

本段小结

其实这篇文章还没有完,超级语言还没有介绍完,我也没说为什么讨厌Java。但是由于篇幅原因,再往这篇文章里添加内容,这里就变得特别臃肿。更多关于超级语言的内容将会再(2)中讲,敬请期待。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇