作者:皮皮关
链接:https://www.zhihu.com/question/335137780/answer/786853293
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
很多游戏开发者都是由于Unity而“被迫”使用C#的。但用过一段时间,就会由衷赞叹:真香。
如果有些同学没感觉到很香,有可能是没有仔细和其它语言比较 :)
1、C#良好兼容了值类型/引用类型,在发展中逐步解决了其他高级语言没解决好的问题
纵观主流语言,C语言在语法上是以值类型为基础,借助指针实现引用类型;而Python/Lua等语言,是以引用类型为基础。
论性能和细节控制力,C语言的设计上限更高;但是论简易程度,Python更为统一、易用。这一基本矛盾在之前的语言里都没有解决好。
而C#很好的总结了前人的经验,在基础语法上就区分了值类型和引用类型。对初次接触编程的同学来说这一点容易造成学习障碍,但是只要掌握了它,就会给实际工作带来极大便利。
反观历史,C#也曾经因为 值类型/引用类型 保守诟病,“拆箱”和“装箱”一直是个招黑的设计。但后来我们看到,随着泛型的成熟和普及,随着泛型容器代替通用容器,装箱和拆箱的问题已经在很大程度上解决了。
还有对异步的支持等等,C#的设计最初带来了一些问题,但是最终还是交上了一份满意的答卷。
2、充分利用栈空间,非常高效,做了一部分C/C++擅长的事
值类型有一大特点,就是能充分利用栈空间。高级语言的GC特性一直饱受诟病,但下面的Unity常见代码,运行时没有GC:
// 通过输入的三维向量,移动物体的位置
void Move(Vector3 input)
{// 演示代码,有意分成很多行input = input.normalized;Vector3 move = input * 2.0f;move *= Time.deltaTime.transform.position += move;
}
这段代码没有在堆上分配空间,你所看到的操作全都是在栈上进行的,GC压力为0。我认为这是C#最令人惊艳的一点。
一般来说数组长度较长,默认分配在堆上。但是C#也提供了便利的语法,在栈上分配数组,对项目后期优化来说简直是神技:
public void unsafe foo()
{int* bar = stackalloc int [10];
}
没错,C#依然保留了指针,但一般仅用于局部的unsafe代码。在局部热点可以完全解放性能。
3、良好的语法设计和库函数设计,引导程序员写出更快且更自然的代码
C#中最常用的容器List,也具有一些良好的设计(当然其它语言也有类似的优点)
// 新建一个list,长度为0。但在堆中预留10万个位置List<int> list = new List<int>(100000);// 加入很多元素,由于容量足够没有GCfor (int i=0; i<89000; i++){list.Add(i);}// 用过以后清空list,长度变成0list.Clear();// 但容量还是10万,继续增加元素还是没有GCfor (int i = 0; i < 99000; i++){list.Add(i);}
list在预留空间充足时,添加元素不会产生GC。而且List和值类型结合使用,在内存占用上也有优势。当然,很多其它语言也有类似的设计,可以说别的语言做的好的部分,C#做的也一样好。
4、继承、泛型、接口、类型约束等等高级特性,都有着良好且自洽的设计
最初接触C#的时候,看看int的原型,收获很大:
public struct Int32 : IFormattable, IConvertible, IComparable, IComparable<Int32>, IEquatable<Int32>{// ....}
熟悉C++的人,经过思考,可以很好的理解IComparable、IEquatable以及它们的泛型形式。同时也能猜出“Interface”的概念。思考C#的底层设计,给人的感觉就是自然、规范、恰到好处。
总之,C#语言及其标准库的设计,非常值得借鉴和推崇。
C#的良好设计让它在游戏开发领域走出了一条光明大道,在其它领域也有着越来越广泛的应用。
C#出现较晚,算是当今所有语言的集大成者。现在它的发展主要受市场环境制约。也许几年以后,会有新的语言在它的基础上更上一层楼 :)
编辑于 2019-08-13
赞同 29870 条评论
分享
收藏喜欢收起
蒋国纲
技术宅男
76 人赞同了该回答
多接触几种语言,你会发现那些令其它语言开发者大呼惊艳的语言新特性其实都是C#玩剩的,我想这就是C#最惊艳的地方
发布于 2019-07-16
赞同 7617 条评论
分享
收藏喜欢
懒得勤快
masuit.com,互联网分享精神,勤于发现,乐于分享。
专业 已有 1 人赠与了专业徽章
208 人赞同了该回答
作为一名集.NET、JavaEE、web前端于一身的全栈开发者,我自认为我对C#、java、javascript的认知都不算很浅的了,如果我们可以同时拥有 C# 和 Java 世界的最好特性,那会是什么样呢?
完美的编程语言并不存在,我希望我们可以在这一点上达成一致。开发新语言往往是为了克服另一种语言的弊端,又不可避免的在某些方面上健壮一些,却在另一些方面上存在不足。
C# 与 Java 都起源于 C/C++ 语言,他们在面向对象方面有许多相似之处。除了 Java JVM 和 C# .NET CLR 有许多相同结构上的相似性之外,他们各自的开发团队都有各自的发展方向,他们关注的是各自的语言应该成为什么样子。
我们并不想纠结于某一个语言比另一个语言好,我们只想罗列出 C# 开发者能用到而 Java 中没有的那些特性而已。
下面我们开始吧。
1. LINQ
LINQ (Language-Integrated Query,语言集成查询) 于 2007 年引入到 C#,以帮助开发人员从各种数据源查询数据。使用它,我们可以在无需考虑正在调用的特定数据库的语法来编写查询语句。LINQ provider 所提供的一个组件将查询转换为下层数据源可读的格式。例如,如果我们需要从 SQL 数据库查询数据,LINQ to SQL provider 程序将把 LINQ 查询转换成 T-SQL,以便数据库可以理解它。
要在 LINQ 中执行查询操作,首先获取数据库,然后创建查询,最后执行查询。在 LINQ to Object 查询中,这可能仅像一样代码一样简单,而不是为每个循环编写嵌套的复杂迭代。
例如,我们来看看这个代码,用于在 C# 中从列表中过滤 2 位数。
首先,在不使用 LINQ 的情况下:
List<int> FilterTwoDigitNumbersWithoutLinq(List<int> numbers)
{var tens = new List<int>();for (var i=0; i < numbers.Count(); i++){if ((9 < numbers[i]) && (numbers[i] < 100)){tens.Add(numbers[i]);}}return tens;
}
如果使用 LINQ 查询语法形式:
List<int> FilterTwoDigitNumbersWithLinq(List<int> numbers)=>(from a in numbers where (a > 9 && a < 100) select a).ToList();
或者是方法语法形式:
List<int> FilterNonTwoDigitNumbersWithLinq2(List<int> numbers)=> numbers.Where(a => a > 9 && a < 100).ToList();
这里两种语法都是正确的,唯一的区别就是查询语法看起来更像是 SQL 语句而方法语法使用 lambda 表达式(当然,看起来很像我们在 Java 里写的某些代码)
综述:LINQ 所依赖的许多特性,如 lambda 表达式(就 LINQ 来说非常有用),已经在 Java 中有了等效的实现,尽管我们可以使用流和 lambda 来查询数据,但 LINQ 简化了整个过程并且移除了很多在 Java 中存在的冗余代码。
2. Struct
C# 中的结构体类似于类。实际上,一个 struct 甚至可以被认为是一个“轻量级类”,因为它可以包含构造函数、常量、方法等等。一个结构体和一个类之间最大的区别在于结构是值类型,而类是引用类型。
相比于创建类,编写结构体最重要的好处是在构造一个值类型时比在构造引用类型时更容易确保值语义。如 Microsoft 的文档所述,“struct 类型的变量直接包含结构体的数据,而类类型的变量包含对数据的引用。”因此,对比使用类时,使用结构体的好处之一是,从代码的其他部分更改其值的唯一方法是将其作为参考进行显式传递。
微软的开发人员建议对于那些小于 16 字节、生命周期短、不改变的而且不常装箱的类型,使用结构体(struct)而不是类(class)。在这种情况下,使用结构体可能会比使用类更有效率,因为它会保存在栈而不是堆中。
比如:
public struct Point
{public int X;public int Y;public Point(int X, int Y){this.X = X;this.Y = Y;}public static Point operator +(Point p1, Point p2){return new Point(p1.X + p2.X, p1.Y + p2.Y);}public override string ToString(){return ($"({X}, {Y})");}
}
class Program
{static void Main(string[] args){Point point1 = new Point(1, 5);Point point2 = new Point(2, 3); Console.WriteLine("两个点相加的结果是: {0}", (point1 + point2)); Console.ReadKey();}
}
小结:很多情况下使用结构体可以节省内存分配和释放的时间,这确实很有吸引力。然而事实是值类型拥有自己的存储空间。无论结构体拥有如何明显的优点和缺点,这在 Java 中都不需要操心。
3. async/await
在一段代码中调用 async,或者更明确地调用方法,这个方法都会在另一个线程上执行,不会阻塞当前线程。当代码运行到 await 命令的时候,它会继续运行(await 的语句)。如果这时 async 代码还没有完成,那么执行中的程序会返回到调用点。
这有助于提高应用程序总体的响应速度,以及减少性能瓶颈。在应用程序访问 Web 和进行所有 UI 相关的活动时,使用异步程序非常重要。相对于以前的异步编程实现,使用 async/await 可以保留你代码的逻辑结构,而编译器则会担负起以前由开发者担负的重担。
示例:
class Program{public static void Main(){Console.WriteLine("Hey David, How much is 98745 divided by 7?");Task<int> david = ThinkAboutIt();Console.WriteLine("While he thinks, lets chat about the weather for a bit.");Console.WriteLine("Do you think it's going to rain tomorrow?");Console.WriteLine("No, I think it should be sunny.");david.Wait();var davidsAnswer = david.Result;Console.WriteLine($"David: {davidsAnswer}");Console.ReadKey();}private static async Task<int> ThinkAboutIt(){await ReadTheManual();Console.WriteLine("Think I got it.");return (98745 / 7);}private static async Task ReadTheManual(){string file = @"D:\HowToCalc.txt";Console.WriteLine("Reading a manual.");using (StreamReader reader = new StreamReader(file)){string text = await reader.ReadToEndAsync();}Console.WriteLine("Done.");}
}
输出:
// Possible Output:
Hey David, How much is 98745 divided by 7?
Reading a manual.
While he thinks, lets chat about the weather for a bit.
Do you think it's going to rain tomorrow?
No, I think it should be sunny.
Done.
Think I got it.
David: 14106
概要:CompletableFutures 无疑可以使我们更趋近于拥有等效于 C# 和 Java 所拥有的异步编程中的能力。尽管如此,使用它所带来的复杂性使其易用度不能与使用 async /await 关键字进行的实现相提并论。
4. Lazy<T> 类
无论使用 C# 还是 Java,很多人都已经实现了延迟初始化 (或实例化),因此对象要在第一次使用的时候才会被创建。有一种常见的例子是将延迟初始化用于应用程序启动的时候加载大量对象,但实际需要初始化的对象可能只有少数几个。这种情况下,我们希望辨别哪些是不需要在这里初始化的。只初始化那些确实需要初始化的对象可以提升应用程序的性能。
小结:最近,Lambda 表达式引入到 Java 8 之后,在 Java 中实现延迟加载(还有不少其它事情)变得更容易了。不过,在 C# 中我们可以使用语义化的 Lazy<T> 封装类来延迟初始化任何类库或用户指定的类型。
5. 一些等价的关键词
语言中的有用功能不一定像在 C# 中的 LINQ 或 Java 中的模块一样大。这里有一些可以帮助 C# 开发人员的关键字,它们在 Java 中并没有:
a. as
C# 中的 as 关键字会尝试安全地将对象转换为某个类型,如果不能转换的话,就返回 null。与 Java 的instanceof 几乎等同,但它是一个布尔值,如果类型匹配则返回 true,否则返回 false。
b. yield
在 C# 中使用 Yield 和 return yield 来进行自定义且状态化的迭代,不需要显式创建额外的类,也不需要创建临时集合。在 Java 中我们实现迭代最好的选择是使用外部库或使用 Java 8 引入的 Lambda 表达式。
c. var
var 是一种自动推断类型,也可以称之为万能接口,其实际类型由编译器决定,其功能相当于写一个显式类型 (比如 int, string 等)。它除了可以减少一些按键之外,var 还允许用于匿名类型,而匿名类型在 LINQ 中很常用。我们期待看到“var”标识,备受瞩目的 Java SE 9 将实现“将类型推导扩展到定义并初始化局部变量时。”
d. checked
C# 中,我们使用 checked 关键字显式启用对整型表达式的溢出检查。如果表达式的运算结果超出目标类型的范围,我们可以使用 checked 强制要求运行时抛出 OverflowException。这十分有用,因为常量表达式会在编译期进行溢出检查,而非常量表达式不会。
工具生态系统
Java 和 C# 之间存在大量的不同之外,当然,其中一些源于 Java 和 .NET 框架的不同。这些不同之处也导致了一些工具在兼容性方面的差异,比如 OverOps 在生产监控和错误跟踪方面的差异。
OverOps 向开发者展示生产中每个错误整个调用栈的全部源代码和变量状态。目前在 .NET 框架上并没有与之相同的内容,不过在接下来的几个月内会有一些变化。想了解更多信息,请点击这里加入我们 .NET Beta 的等候名单,如果你是 Java 开发者可以去 http://www.overops.com 查看演示。
最后的思考
在快结束时候,我们这里提到的大部分功能都在代码长度和简洁程度方面对 C# 开发者有所帮助,这些代码不能在 Java 中编写。事实上这些特性也或多或少说明了 Java 语言冗长的问题,包括最近版本更新带来的 Lambda 表达式。诚然,很多这些存在于 C# 而不存在于Java 中的特性在常规使用中提供了比使用 Lambda 更简洁的语法。
再次说明,我们不想卷入没完没了的关于哪种言更好的争论,我们只是在这里指出两种语言之间的一些区别。我们是否遗漏了某些你希望 Java 拥有的特性?请在评论中告诉我们!