- 说一下你最喜欢语言的三个缺陷
- 为什么现在对函数式编程语言越来越受到欢迎?
- 什么是闭包,闭包有什么作用?它和类有什么区别?
- 什么是高阶函数?它是用来做什么的?用你最喜欢的语言写一个高阶函数;
- 编写一个循环,然后将它转换成递归的形式,并且只能使用不可变结构(比如避免使用变量)
- 什么是栈和堆?什么叫栈溢出?
- 命名空间(namespace)是做什么的?能够发明一个可替代性的东西?
- 编写两个函数,一个是引用透明(Referentially Transparent),另一个是引用不透明(Referentially Opaque)
- 为什么在有些语言设计中没有异常机制?那么它们有什么优势和弊端?
- 为什么在 JAVA,或者 C#中,构造函数不是接口的一部分?
- 泛型是用来做什么的?
- 语言将函数当做一等公民意味什么?
- 展示一个例子来说明匿名函数是有用的;
- 有许多不同的类型系统:静态类型和动态类型,强类型和弱类型等等。你能分享和讨论一下在开发一个企业级软件的时候,如何去选择特定的类型系统?
- 讨论一下 JAVA 和 C#之间的互通性?
- 为什么许多开发人员不喜欢 JAVA?
- 好语言的好和坏语言的坏各自在什么地方?
- 在一些语言中,尤其是函数化倾向的语言中,有一种叫模式匹配(Pattern Matching)的技术,那么在模式匹配和 Switch 语言有什么区别?
- 如果 Cat 是 Animal,那么设计的时候是 TakeCare 还是 TakeCare?
- 最近几年,有很多关于 Node 的不实的宣传,那么你对这些原本运行在浏览器中的语言用作后端开发语言的看法是什么?
- 假设你有一台时光机,能够穿梭到 Java 语言创建的时间点,并且能够和 JDK 的架构者交流,那么你将会说服他什么?比如移除检查异常(checked exception)机制?增加非符号的的基础类型?增加多继承?
C# 语言
- 开源太晚
虽然说现在 C#
已经开源,但是面对互联网,云计算和大数据时代还是显得有点太晚了。在一些技术社区仍然以为 C# 知识 Windows Only 的开发语言。这个对于新的开发者而言是一个很错误的引导,而且吸引不了更多的开发者进入这个行业。而且在过去几年,C# 开发领域由很多令人困惑的概念,比如说 .Net Core
, .Net Standard
, Mono
, .Net 5
以及传统的 .Net Framework
。这些概念对于有经验的开发工程师都难以区分它们。
- 复杂的比较操作
在 C# 中有很多比较操作,这些操作往往令开发者难以区分。
- 用户自定一个比较操作:
>
,<
,>=
,==
,!=
等等 - 重载
Equals(object)
这个方法 Object
类中包含的Equals
这个静态方法IComparable
接口IEquality
接口
- 对接口限制太严格
对于接口,只能包含了方法和属性,不能包含字段,静态方法等等。
大部分应用程序在开发的过程中的缺陷主要是由软件开发者并没有完整的清楚代码在实际运行时候全部的状态。
尤其是在多线程运行环境中,这个问题就会被放大。通过函数式编程软件中所有的状态就会变得明确,同样使得
诸如多线程的条件竞争等问题得到解决。纯函数是函数式编程重要的内容,它只关注传递给他的参数,返回根据
传入的参数计算而得的值,没有逻辑上的副作用(side effect
)。它不更新全局变量,不维持全局变量,
也不会进行 IO 操作,更不会修改传入的参数。纯函数的有一下几点优势
- 线程安全:纯函数只使用参数,所以它是完全线程安全的的;所以很容易地将这些函数改造成并行执行,尤其在多核 CPU 中发挥优势;
- 可重用性:将纯函数转移到新的环境非常简单,只需要处理类型定义和函数调用,不会发生类似滚雪球效应;
- 可测试性:纯函数是引用透明的,也就是说同样的参数调用无论如何都会返回正确的结果;
- 可理解性和可维护性:只关心参数的输入和结果的输出,大大降低了维护者的理解难度
闭包(Closure)是词法闭包(Lexical Closure)的简称,闭包提供了一种方位内部变量的一种方式。 程序设计中,每一个变量都有一定的作用域,作用域之外的将不能访问该该变量
var a = 1
func func1() {
var b = 10
fmt.Printf("%d", a)
}
// error
func func2(){
fmt.Printf("%d", b)
}
变量a
是全局变量,所以对func1
和func2
都可见,但是变量b
是局部变量,对于func2
是不可见的,所以无法访问b
。这是由go
语言的"链式作用域"结构(chain scope
)决定的。子对象会一级一级地向上寻找所有父对象的变量,所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
但是闭包提供了方位局部变量的方法:
func func3() func() {
var c = 10
return func(){
c++
fmt.Printf("%d", c)
}
}
handler := func3()
handler() // 11
handler() // 12
在go
语言中,函数是一等公民(first class citizen
),所以func3
可以返回一个函数。该函数包含了局部变量c
,所以在外面就可以访问c
变量的方式。
使用闭包的可以减少变量的使用,使用局部变量就可以保存全部的状态。
如何一个函数接受另一个函数作为参数或者返回函数,那么这个函数就是高阶函数。 高阶函数能够表达出更强的抽象。
func twice(f func(int) int, v int) int {
return f(f(v))
}
func main() {
f := func(v int) int {
return v + 3
}
twice(f, 7) // returns 13
}
func factorialInter(n int) int {
val := 1
for i:=1; i<n; i++ {
val = val * i
}
return val
}
func factorialRec(n int, val int) int {
if n = 1 {
return val
}else{
return factorialRec(n-1, val*n)
}
}
由于虚拟内存的设计,每一个应用程序 "仿佛" 使用了全部机器的内存,一般来讲程序内存划分情况如下图:
- 地址
0xffffffff - 0xc0000000
为内核地址; - 地址
0x08048000
往上为程序的只读段,主要包含了代码段,只读数据段,再往上为数据段; - 再往上为堆,所有程序中手动分配的内存将在这个位置开始往上分配;
- 地址
0x40000000
往上一部分为动态共享库; - 地址
0xc0000000
往下为栈空间。
操作系统为为每一个函数调用提供了栈帧,主要保存函数的参数、返回地址和一些局部变量。如果程序设计不当,将会导致栈帧使用完毕,导致栈溢出,常见的主要有无限递归。
在编程领域中,命令空间主要解决变量,函数以及类它们之间的冲突。假设你的应用程序使用了两个库。
// library A
public class FooBar
{
//...
}
// library B
public class FooBar
{
//...
}
我们可以看到,FooBar
两个类出现了两个库,我们的应用程序编译器不能区分它们,所以我们引入了命名空间。
// libray A
namespace Tindo.SDK
{
public class FooBar {}
}
// library B
namespace Aurisoft.Tool
{
public class FooBar {}
}
现在我们就能很好的区分它们,不会发生对象解析错误。
如果我们能够解析冲突,就不需要命令空间。我们可以为每个库的对象能唯一值标记即可。
// libaray A -> libraryA.dll
public class FooBar {}
// library B -> libraryB.dll
public class FooBar {}
// programa
using FooBarA = import("libraryA.dll", "FooBar");
using FooBarB = import("libraryB.dll", "FooBar");
首先什么是引用透明呢? 它用来描述定义一个表达式的事实,在一个程序中,如果一个表达式可以被一个具体的值取代而不影响结果那么我们就可以称为改引用透明,从某种程度来讲就是改表达式由特定的参数输入一定会输出相同的结果,这是函数式编程的概念。
假设我们由下面几个函数
int Add(int a, int b){
return a + b;
}
int Mult(int a, int b){
return a * b;
}
int x = Add(2, Mult(3, 4));
在上面的例子中, Mult
函数就是引用透明的,因为我们可以用 12
替换掉 Mult(3, 4)
而不会有任何影响;同样我们也可以用 14
替换 Add(2, 12)
。
下面再介绍一下引用不透明的例子
int Add(int a, int b){
int result = a + b;
Console.WriteLine($"Returning {result}");
return result;
}
如果我们用特定的值替换 Add
方法,那么 Returning
方法就不会输出,产生了副作用 (Side Effect)。有些情况下,非但带来了副作用,而且导致的结果也不正确。
class Fibs {
private int previous = 1;
private int last = 1;
public int Next() {
last = previous + (previous = last);
return prevous + last;
}
}
public void PrintFibs(int limit){
Fibs fibs = new Fibs();
for(int i =0; i < limit; i++){
Console.WriteLine(fibs.Next());
}
}
在这里我们不能用任何值代替 Next
方法的调用,因为这个方法在每次调用的时候就是不一样的。
异常的设计是随着语言的发展而发展的,在早期的语言,比如汇编语言并没有应用程序层面的异常,只需要顺序和跳转两中执行控制语句就可以完成全部的工作。现代语言通常封装了底层的逻辑,因此也提供了大量的高级的语言的特性,所以异常自然而然提出。
优势
- 异常可以将错误处理代码和正常的逻辑流区分开来,这样代码就可以很容易的阅读,健壮和可拓展性
举个例子如下
使用异常处理
// sample 1: A function that uses exceptions
string get_html(const char* url, int port)
{
Socket client(AF_INET, SOCK_STREAM, IPPROTO_TCP);
client.connect(url, port);
stringstream request_stream;
request_stream << "GET / HTTP/1.1\r\nHost: "
<< url << "\r\nConnection: Close\r\n\r\n";
client.send(request_stream.str());
return client.receive();
}
如果使用错误代码
// sample 2: A function that uses error codes
Socket::Err_code get_html(const char* url, int port, string* result)
{
Socket client;
Socket::Err_code err = client.init(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (err) return err;
err = client.connect(url, port);
if (err) return err;
stringstream request_stream;
request_stream << "GET / HTTP/1.1\r\nHost: " << url
<< "\r\nConnection: Close\r\n\r\n";
err = client.send(request_stream.str());
if (err) return err;
return client.receive(result);
}
上面的两个例子都是完成同样的事情,但是从错误处理的版本来看,代码需要处理很多错误的情况,使代码的可读性不高。
- 只有抛出异常才能解决构造函数的的错误
在类的构造函数中,通常需要申请一些系统资源,如果申请失败,就会让对象处于不稳定的状态。如果使用错误处理就不能处理这种情况。
- 异常很难被忽略
对于没有捕获的异常,系统就会 crash 掉,这个有助于系统维护者更早的发现发现问题,解决问题,而不是忽略它们。但是对于错误处理这种方式,就很难做到。
- 异常可以从嵌套的函数中传播出来
使用异常可以很方便地将错误从发生地地方到最外面调用地地方。
- 异常可以使用自定义地类型,它们可以比错误代码包含更多地信息
通常错误代码使用整型,而且没有更多的信息。当然可以为错误代码设计为一个 Object,但是需要复制很多次这种对象。但是异常的话,本身就是一个对象,而且可以通过类型系统来处理它们。
// Exception handler
void AppDialog::on_button()
{
try {
string url = url_text_control.get_text();
result_pane.set_text(
get_title(url));
}
catch(Socket::SocketConnectionException& sock_conn_exc) {
display_network_connection_error_message();
}
catch(Socket::Exception& sock_exc) {
display_general_network_error_message();
}
catch(Parser::Exception& pars_exc) {
display_parser_errorMessage();
}
catch(...) {
display_unknown_error_message();
}
}
通过异常类型的条件,处理不同的情况。
劣势
- 异常为代码逻辑增加了很多不可见的退出点,这样的增加的代码检查的难度。
以为异常可以跳出正在正在执行的逻辑代码流,所以在无形之中为代码函数增加了退出的机制
- 异常可能导致资源泄露,尤其使没有内置垃圾回收的编程语言
由于异常可以提前退出整个代码逻辑,所以可能导致已经分配系统资源的没有执行释放代码。
- 异常的发生会导致性能上的损失
众所周知,异常对系统的性能是由损失的。
构造函数是特殊的成员函数方法用来初始化新创建的对象,它会在类对象创建的时候自动被调用。但是构造函数并不是接口的一部分。
-
接口是类的完全的抽象,在 Java 中接口中的所有的数据成员都是默认
public
,static
并且final
修饰的。所有的静态字段必须在声明的时候就应当赋值,否则就会出现编译的时候错误。 -
接口中的方法都是默认
public
的,而且也是abstract
这就意味着在接口中不应当提供具体的实现,应当由实现接口的类来完成这部分工作。因此,没有必要再接口中包含一个构造方法。 -
构造函数用来初始化非静态的数据成员,因为在接口中没有非静态的数据成员,所以没有构造函数的必要。
-
接口中的方法只有声明没有定义,由于没有方法的具体实现,所以没有必要有一个对象来调用方法同样也不会有构造函数。
泛型编程主要是解决下面三个问题
- 算法的泛型;
- 类型的泛型;
- 数据结构的泛型。
假设我们这里有一个 Search
的方法,它的主要作用是在一个集合中查找一个特定的元素
int search(void* a, size_t size, void* target,
size_t elem_size, int(*cmpFn)(void*, void*) )
{
for(int i=0; i<size; i++) {
if ( cmpFn (a + elem_size * i, target) == 0 ) {
return i;
}
}
return -1;
}
这里 C
语言实现有个重要的问题是它不适用非线性数据结构,比如像哈希表,二叉树等等,算法返回的索引值并没有实际意义,而且 i++
也没有具体的意义。
那么如果使用 C++
模板的泛型来实现是怎样的的?
template<typename T, typename Iter>
Iter search(Iter pStart, Iter pEnd, T target)
{
for(Iter p = pStart; p != pEnd; p++) {
if ( *p == target )
return p;
}
return NULL;
}
这里有个泛型参数
- T 用来表示集合中数据的类型
- Iter 用来表示迭代器,从而支持了不同集合的类型。
这个也是 C++
的 STL
使用的泛型方法,其中包括了
- 泛型的数据容器
- 泛型数据容器的迭代器
- 泛型的算法
泛型还解决了更高维度的抽象,因为我们希望这个算法只管遍历,具体要干什么,那么业务逻辑,由外面的调用方法定义就好了,这样代码的重用度就提高了。
template<class Iter, class T, class Op>
T reduce (Iter start, Iter end, T init, Op op) {
T result = init;
while ( start != end ) {
result = op( result, *start );
start++;
}
return result;
}
这里整个迭代器传入了一个 operation
, 在迭代的每一步都是由传入的操作完成。在定义了这个 reduce
函数后,我们可以从 Employees
列表中获取总薪水和最高薪水。
double sum_salaries =
reduce(staff.begin(), staff.end(), 0.0,
[](double s, Employee e) { return s + e.salary; });
double max_salary =
reduce(staff.begin(), staff.end(), 0.0,
[](double s, Employee e) { return s > e.salary ? s : e.salary; });
在了解了 C++
的泛型的例子后,我们来探讨一下泛型的本质。在编程世界中,我们需要做好两件事:
- 编程语言中的类型问题
- 对真实世界中业务代码的抽象,重用和拼装
一般来讲,编程语言会有两种类型,一是内建类型,比如 int, float 和 char 类型;另一种是抽象类型,比如 struct, class 和 function 类型。程序的语言的系统主要提供了下面的功能
- 程序语言的安全性:使用类型可以让编译器检测到一些代码的错误
- 有利于编译器优化:可以让编译器明确的知道程序员的意图,利用这些这些信息编译器可以做很多的代码的优化的工作;
- 代码的可读性:让代码更容易阅读和维护,代码的语义也更清楚,代码的模块的接口也更加丰富和清楚。
- 抽象化:可以让开发人员以较高层次的方式思考,而不是底层的细节
总结来讲
- 类型是对了内存的抽象。不同的类型,会有不同的内存布局和内存分配的策略
- 不同的类型,有不同的操作,所以对于特定的类型,也有特定的一组的操作。
对于泛型需要做到如下的事情
- 标准化类型的内存操作,访问和访问
- 标准化内存的操作
- 标准化数据的容器的操作
- 标准化类型上的特有的操作
在编程语言中,将函数视为一等公民(first-class citizens)意味着函数被当作一种可以存储在变量中、可以作为参数传递给其他函数、可以作为其他函数的返回结果,并且可以像任何其他数据类型一样具有能力的实体。这个概念是函数式编程范式的核心特征之一,但也在许多支持多范式的编程语言中实现。
当一个语言将函数视为一等公民时,这带来了几个重要的好处:
-
更高的表达力:能够将函数赋值给变量,或者作为参数和返回值,提供了强大的表达能力,使得编写高度抽象和模块化的代码成为可能。
-
支持高阶函数:由于函数可以作为参数和返回值,这允许编写高阶函数(higher-order functions),即那些接收一个或多个函数作为参数,或者返回另一个函数的函数。高阶函数是许多强大编程模式和技术的基础,比如映射(map)、过滤(filter)、折叠(fold)等。
-
促进函数式编程:函数作为一等公民是函数式编程范式的关键特征之一。这促进了不可变性、声明式编程、函数组合等函数式编程的核心概念。
-
简化回调和异步编程:在需要异步操作或回调时,能够将函数作为参数传递使得事件处理、异步调用和处理更为简洁和灵活。
-
便于抽象和重用代码:通过函数抽象,可以轻松封装和重用代码逻辑,提高了代码的复用性和模块化程度。
举例来说,在JavaScript这样将函数视为一等公民的语言中,你可以这样做:
// 将函数赋值给变量
const greet = function(name) {
return "Hello, " + name + "!";
};
// 将函数作为参数传递
function processUserInput(callback) {
var name = "Alice";
console.log(callback(name));
}
// 调用processUserInput,将greet函数作为回调函数传递
processUserInput(greet);
匿名函数(也称为lambda表达式或lambda函数)在许多编程场景中都非常有用,特别是在需要简短的函数作为参数传递给其他函数时。匿名函数提供了一种快速定义和传递行为的方式,无需正式定义函数。这在进行事件处理、数据处理、或者使用高阶函数时尤其方便。
下面是一个使用JavaScript的例子,说明了在数组操作中使用匿名函数来进行元素的过滤和映射:
// 假设我们有一个数字数组,我们想要过滤出其中的偶数,然后将它们翻倍
const numbers = [1, 2, 3, 4, 5, 6];
// 使用匿名函数过滤偶数
const evenNumbers = numbers.filter(function(number) {
return number % 2 === 0;
});
// 使用匿名函数将过滤后的偶数翻倍
const doubledEvenNumbers = evenNumbers.map(function(number) {
return number * 2;
});
console.log(doubledEvenNumbers); // 输出: [4, 8, 12]
在这个例子中,我们首先使用filter方法和一个匿名函数来选出数组中的偶数。接着,我们使用map方法和另一个匿名函数将这些偶数翻倍。在这两种情况下,匿名函数都是临时定义的,用完即丢,非常适合执行简单的操作,无需在代码的其他部分定义或调用。
这种方式使得代码更加简洁且易于理解,特别是在处理集合、事件监听器或异步操作时。匿名函数避免了为了一次性的简单操作而需要定义一个完整的命名函数的额外开销,从而使代码保持简洁和专注于当前逻辑。
在选择适用于企业级软件的类型系统时,重要的是要根据项目的具体需求、团队的经验、项目规模以及长期维护的考虑来决定。静态类型系统和动态类型系统,以及强类型和弱类型系统,都有其优势和局限性。理解这些特性将帮助你做出更加适合项目需求的决策。
静态类型系统 vs 动态类型系统 静态类型系统在编译时检查类型。这意味着类型错误会在代码运行之前被发现,有助于早期识别问题,这对于大型项目而言是一个显著的优势。静态类型的语言包括Java、C#、TypeScript等。
- 优势:
- 提早发现错误:编译时类型检查有助于尽早发现潜在的错误。
- 更好的性能:类型在编译时已知,可以生成优化的机器码。
- 工具支持:提供更好的自动完成、重构工具和类型推断等。
- 局限性:
- 更严格的开发过程:需要提前声明变量类型,可能减慢初期开发速度。
- 学习曲线:对初学者来说可能更加复杂。
- 动态类型系统在运行时检查类型。这提供了更大的灵活性和更简洁的代码,但也可能导致运行时错误。动态类型的语言包括Python、Ruby和JavaScript等。
- 优势:
- 灵活性:代码更灵活,易于快速原型开发。
- 简洁的语法:通常不需要声明类型,使得代码更简洁。
- 局限性:
- 运行时错误:类型错误可能直到运行时才被发现。
- 性能开销:运行时类型检查可能会导致性能损失。
- 强类型系统 vs 弱类型系统
- 强类型系统不允许隐式类型转换,或者其规则非常严格。这有助于避免类型相关的逻辑错误。强类型语言的例子包括Python和Java。
弱类型系统允许更宽松的类型转换,这可以提高灵活性,但也增加了出错的风险。JavaScript是一个允许隐式类型转换的弱类型语言的例子。
选择类型系统 在选择适合企业级软件的类型系统时,以下因素应当被考虑:
项目规模和复杂性:大型或复杂项目可能会从静态类型系统中获益,因为它有助于管理复杂性并提高代码的可维护性。 团队经验:如果团队成员对特定类型系统有更多经验,那么选择与团队技能相匹配的语言可能更加高效。 开发速度和迭代速度:对于需要快速迭代的项目,动态类型语言可能更合适。 性能需求:如果应用对性能有严格要求,静态类型语言可能提供优势,因为它们通常能生成更优化的代码。 长期维护:静态类型系统可以提供更好的文档性质(通过类型签名),这对于长期维护大型代码库非常有用。 最终,没有一种类型系统适合所有情况。选择哪种类型系统应基于项目的具体需求、团队偏好和长期目标进行仔细考虑。
Java和C#是两种广泛使用的编程语言,它们都是静态类型的、面向对象的高级编程语言,由不同的公司开发和支持(Java最初由Sun Microsystems开发,现在由Oracle维护;C#由Microsoft开发)。尽管它们在语法和设计理念上有许多相似之处,但它们运行在不同的平台上(Java主要运行在Java虚拟机上,而C#运行在.NET环境上),这使得直接的互通性存在一定的挑战。然而,存在多种方式可以实现Java和C#之间的互通性,每种方法都有其特定的应用场景和限制。
-
使用Web服务 一种在Java和C#应用程序之间实现互通性的流行方法是通过Web服务。这可以是SOAP基于XML的服务,或者是更轻量级的RESTful服务使用JSON作为数据交换格式。Web服务提供了一种语言无关的方式来允许不同的系统通过HTTP进行通信,这意味着Java和C#应用程序可以轻松共享数据和功能。
-
使用跨平台框架 一些框架和工具旨在促进不同语言之间的互通性,包括Java和C#。例如,Apache Thrift和Google的Protocol Buffers支持多种编程语言,允许开发人员定义数据类型和服务接口,然后自动生成Java、C#等语言的源代码。这些工具主要用于构建高性能的跨语言服务。
-
使用中间件 中间件产品如消息队列(例如RabbitMQ、Apache Kafka)和企业服务总线(ESB)也可以用来在Java和C#应用程序之间进行通信。这些中间件支持异步消息传递,是实现不同语言编写的系统之间解耦合通信的一种有效方法。
-
互操作框架 有一些专门的框架和库被设计用来在Java和.NET平台之间提供更直接的互操作性。例如,JNBridge是一个商业产品,允许Java和.NET应用程序相互调用对方的API。这种类型的工具通常通过在两个环境之间创建一个桥接层来工作,允许直接调用和数据交换。
-
使用公共语言运行时(CLR)和Java虚拟机(JVM)的互操作 虽然这是一种更复杂和不常见的方法,但理论上可以通过特定的技术在CLR和JVM之间建立直接的互操作性。这种方法通常涉及到更多的底层编程和对两个平台内部工作机制的深入了解。
总结 尽管Java和C#设计为运行在不同的平台上,但通过上述方法,它们之间的互通性是可实现的。选择哪种方法取决于具体的应用需求、性能考虑、开发和维护成本以及安全性需求。在许多情况下,使用Web服务或跨平台框架将是实现Java和C#之间通信的最简单和最直接的方法。
Java 是一门广泛使用的开发语言,长期霸占 TIOBE
榜单的头名,正如一句名言所说
没有好语言和坏语言,只有没人用的语言和被人骂的语言
既然 Java 被广泛使用,因此也会被广泛批评,主要观点如下
-
Java 太慢了有严重的性能问题 和 C/C++ 相比,Java 消耗了大量的内存但是运行还是显著的缓慢。主要原因是作为运行在虚拟机上的语言,需要增加额外的编译时间,而且还是在抽象层的 JVM 上运行。
-
Java GUI 功能太弱了 尽管 JAVA 有 Swing, SWT 等,但是它们的功能实在是太弱了,不能开发负责的 UI, 而且它们还是不是很稳定。
-
Java 需要大量的内存空间 由于 GC 的特性,Java 在运行时候需要更大的内存空间,尤其与 C/C++ 这样的语言比起来而言。在 GC 运行的时候,内存使用效率非常低。
-
繁琐和复杂的代码 Java 代码非常啰嗦,也就是说为了完成一个简单的功能,需要写很多长的代码。这些导致了理解它们非常耗时。
评价一门编程语言是否"好"或"坏"很大程度上取决于它在特定上下文、对特定问题的适应性,以及它与开发者目标的契合度。没有绝对的好坏之分,但是我们可以根据一些普遍接受的标准和特性来讨论编程语言的优势和劣势。
- 好语言的特点
- 可读性和简洁性:好的编程语言应该易于阅读和理解,允许开发者用简洁的代码表达复杂的概念。
- 强大的标准库和工具支持:丰富的标准库和强大的开发工具可以极大提高开发效率和程序的稳定性。
- 良好的社区支持:活跃的开发社区可以为开发者提供问题解决方案、最佳实践和第三方库,有助于快速学习和问题解决。
- 可维护性和可扩展性:好的语言设计鼓励编写可维护和可扩展的代码,例如通过模块化和面向对象的设计原则。
- 跨平台能力:能够在多种操作系统和环境中运行的语言具有更广泛的应用范围。
- 性能和效率:对于需要高性能的应用,好的编程语言能提供优化工具和技术来满足这些需求。
- 安全性:提供防止常见漏洞的机制,如内存管理和数据类型检查,有助于编写安全的代码。
- 坏语言的劣势
- 学习曲线陡峭:复杂的语法或缺乏清晰的文档可以使得语言难以学习和掌握。
- 有限的社区和资源:缺乏一个活跃的开发社区和丰富的学习资源会限制开发者解决问题和学习新技术的能力。
- 可维护性差:如果语言设计不鼓励清晰和模块化的代码编写,可能会导致项目难以维护和扩展。
- 跨平台支持差:如果语言或其实现在不同平台间的兼容性差,会限制软件的可移植性。
- 性能问题:某些语言可能因为设计上的选择或缺乏优化而面临性能瓶颈。
- 安全隐患:缺乏足够的安全机制可能使得编写的应用容易受到攻击。
总的来说,一门"好"的编程语言通常是指它能够高效地解决特定问题,同时提供易于学习、编写、维护和扩展代码的特性。然而,最适合特定项目或团队的语言选择取决于多种因素,包括项目需求、团队技能、可用资源和预期的维护寿命周期。在选择编程语言时,评估其优势和劣势以及它们如何适应你的特定需求是非常重要的。
在计算机科学中,模式匹配是一种行为,它用来检查一系列 Token
是否满足特定的模式条件,它只有是/否的两种选择。
假设你熟悉 if
和 Switch
编程语法,接下来我们以 C#
编程语言支持的模式匹配为例。我们现有有不同的几何图形的类,但是不同于之前的继承关系,我们并没有引入这些概念。
public class Square
{
public double Side { get; }
public Square(double side)
{
Side = side;
}
}
public class Circle
{
public double Radius { get; }
public Circle(double radius)
{
Radius = radius;
}
}
public struct Rectangle
{
public double Length { get; }
public double Height { get; }
public Rectangle(double length, double height)
{
Length = length;
Height = height;
}
}
public class Triangle
{
public double Base { get; }
public double Height { get; }
public Triangle(double @base, double height)
{
Base = @base;
Height = height;
}
}
现在我们有四个集合图形的类,需要来提供了公共的方法来计算它们的面积。
方案 1
采用 is
表达式,这是最经典的表达,通过判断传入的类型来分别计算。
public static double ComputeArea(object shape)
{
if (shape is Square)
{
var s = (Square)shape;
return s.Side * s.Side;
}
else if (shape is Circle)
{
var c = (Circle)shape;
return c.Radius * c.Radius * Math.PI;
}
// elided
throw new ArgumentException(
message: "shape is not a recognized shape",
paramName: nameof(shape));
}
方案 2
使用 Switch
表达式,在之前的 C#
语法中,每个 Switch
中的条件都是常量,而且只能是数字和 String
类型。
public static string GenerateMessage(params string[] parts)
{
switch (parts.Length)
{
case 0:
return "No elements to the input";
case 1:
return $"One element: {parts[0]}";
case 2:
return $"Two elements: {parts[0]}, {parts[1]}";
default:
return $"Many elements. Too many to write";
}
}
但是在模式匹配中,这些限制条件被放开了,只需要每个 case
中的表达式能够返回 True/False
即可
public static double ComputeAreaModernSwitch(object shape)
{
switch (shape)
{
case Square s:
return s.Side * s.Side;
case Circle c:
return c.Radius * c.Radius * Math.PI;
case Rectangle r:
return r.Height * r.Length;
default:
throw new ArgumentException(
message: "shape is not a recognized shape",
paramName: nameof(shape));
}
}
在这里,每个 case
语句不再是限制为 int
和 string
, 而是一个个判断条件。
通过模式匹配,避免了冗长繁琐的 if-else
比较操作,而且丰富的程序的表达形式。
从面向对象的角度来看,如果 Cat
是 Animal
的一个子类,那么应当是选择 TakeCare<Animal>
而不是 TakeCare<Cat>
,主要原因有一下几点:
- 从软件工程的角度来看,我们在实现 API 的时候应当选择: 对输入保持宽容,对输出保持严格, 如果 API 的定义选择了
Animal
, 那么任何继承Animal
的类都可以调用这个 API。 - 从语义的角度来看,使用
Take<Animal>
更加符合直挂的定义;通常我们会在Animal
中定义好动物的行为,比如Sleep
,Drink
或者Eat
等行为,每个具体的Animal
都可以重载这些行为,而TakeCare
方法中不用关心具体的实现。
将原本运行在浏览器中的语言,如JavaScript,用作后端开发的实践,主要通过Node.js实现,已经成为现代Web开发的一个重要组成部分。这种做法有其显著的优势,也面临一些挑战,正是这些特点共同影响了业界对其的看法。
- 优势
- 全栈JavaScript开发:使用JavaScript作为前后端开发语言,可以让开发者使用单一语言进行全栈开发,这降低了学习曲线,同时提高了开发效率和团队协作的便捷性。
- 高性能的非阻塞I/O:Node.js基于事件循环和非阻塞I/O模型,这使得它特别适合处理大量并发连接和I/O密集的应用,比如实时通信应用和数据密集的后端服务。
- 庞大的生态系统:npm(Node Package Manager)是世界上最大的软件注册中心,提供了海量的库和工具,支持快速开发和部署应用。
- 跨平台支持:Node.js支持跨平台开发,使得开发者可以在不同的操作系统上开发和部署应用。
- 挑战
- 回调地狱:虽然现代JavaScript已经通过Promises、async/await等特性缓解了这一点,但在Node.js的早期,处理深层嵌套的回调是一个常见的问题,增加了代码的复杂度。
- CPU密集型任务:Node.js的单线程事件循环模型在处理CPU密集型任务时可能不如多线程模型的语言高效。虽然可以通过创建子进程或使用Worker线程解决,但这增加了复杂性。
- 稳定性和安全性:Node.js和其生态系统中库的快速迭代可能导致稳定性和安全性问题。依赖的管理和更新需要谨慎处理。
看法 将JavaScript用于后端开发,尤其是通过Node.js,是一个创新且成功的实践,它扩展了JavaScript的应用范围,促进了全栈开发模式的流行。它的成功证明了一个语言或技术的适用性不应仅由其起源或最初的设计目的来限定。事实上,技术的发展和应用场景的变化往往能推动语言和工具的进化,使其能够适应新的挑战和需求。
不过,对于任何技术选择,了解其优势和局限性是非常重要的。Node.js(或任何其他技术)并非适合所有类型的项目。选择使用它作为后端开发语言,应当基于项目的具体需求、团队的技能背景,以及预期的应用场景进行综合考量。适当的选择可以最大化技术的优势,而不当的选择可能会导致开发和维护上的困难。
22 假设你有一台时光机,能够穿梭到 Java 语言创建的时间点,并且能够和 JDK 的架构者交流,那么你将会说服他什么?比如移除检查异常(checked exception)机制?增加非符号的的基础类型?增加多继承?
如果我有机会回到Java语言创建的时间点,并与JDK的架构者进行交流,我会谨慎地提出一些建议,以期在不破坏Java核心哲学和设计目标的前提下,使Java成为一个更强大、更易用的语言。下面是一些潜在的讨论点:
- 移除检查异常(Checked Exceptions)
建议理由:虽然检查异常的设计初衷是为了提高软件的健壮性,通过强制处理错误情况来避免潜在的运行时错误,但实践中它经常被批评为增加了代码的复杂性,使得异常处理变得繁琐。这导致了一些开发者通过诸如将检查异常包装为运行时异常(unchecked exceptions)的方式来规避这一机制,从而降低了代码的清晰性和可维护性。 可能的折衷:提供更灵活的异常处理机制,比如增强编译器和工具的支持,引入更细粒度的异常处理规则,或者提供更丰富的运行时异常类别,来平衡检查异常带来的强制性和代码的清晰度。
-
增加无符号的基本类型 建议理由:Java中缺少无符号类型,这在处理需要无符号数据(如大量处理网络协议和文件格式等)时不够方便。虽然可以通过使用更大的数据类型或者额外的逻辑来模拟无符号运算,但这增加了开发的复杂度和运行时的开销。 可能的影响:引入无符号类型可以使Java在更广泛的应用场景中更为有效,例如在需要直接处理硬件或进行底层编程的场合。然而,这也可能增加语言的复杂性,需要仔细考虑如何在保持Java简洁性的同时引入这些特性。
-
增加多继承 建议理由:Java为了避免C++中多继承可能引起的问题(如菱形继承问题),采用了接口来实现多态性,并在Java 8中通过默认方法引入了接口的“多继承”。虽然这种设计有助于保持类的单一职责和系统的整洁,但在某些情况下,开发者可能会发现真正的多继承(允许一个类继承多个具体类)能带来更直接的便利。 可能的折衷:探讨如何在Java中引入更灵活的继承机制,比如通过引入混入(mixins)、特性(traits)或协议(protocols),来提供一种安全的多继承方式,既保留了Java的简洁和安全性,又增加了语言的灵活性和表达力。
总结 这些建议都需要在保持Java核心哲学的前提下谨慎考虑。Java的设计哲学是简洁、面向对象、跨平台和高性能。任何改变都需要平衡新特性的好处和可能引入的复杂性、兼容性问题及对现有代码库的影响。与JDK的架构者进行这样的讨论,不仅是关于语言特性的改进,更是关于如何在保持Java成功的同时,使其继续发展适应新的编程范式和开发者的需求。-