Published on

Java 开发者的 C# 快速入门指南

Authors

长久以来,微软在开发者群群体中的形象一直偏负面,所以对微软的技术一直提不起兴趣。直到最近几年,微软的技术栈才开始逐渐被开发者所接受。

最近做一些东西,需要用到 C#,于是开始学习 C#。在学习过程中,发现 C# 和 Java 有很多相似之处,所以写下这篇文章,供其他 Java 开发者参考。

我不打算写一个大而全的东西,主要是给 Java 开发者一个快速入门的指南,并与 Java 进行对比。

代码排版风格上来讲

C# 喜欢使用 BSD 风格的代码排版,而 Java 则使用 K&R 风格的代码排版。

可以感一下:

using System;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello from C#!");
    }
}
public class Program {
    public static void main(String[] args) {
        System.out.println("Hello from Java!");
    }
}

另外,在方法和类的命名上,C# 更倾向于使用 PascalCase,而 Java 方法则使用 camelCase。

当然,以上只是约定俗,C# 和 Java 都允许使用其他风格的代码排版。

语言特性

属性

C# 提供了属性(Properties)作为字段(Fields)的公共访问方式。这允许你在不暴露字段本身的情况下,控制对字段的访问(读取和写入),而不需要显式编写 getter 和 setter 方法。

public class Person
{
    private int age;
    public int Age
    {
        get => age;
        set => age = value > 0 ? value : throw new ArgumentException();
    }
}

在 Java 中,你需要手动编写 getter 和 setter 方法:

public class Person {
    private int age;
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        if (age > 0) {
            this.age = age;
        } else {
            throw new IllegalArgumentException();
        }
    }
}

使用:person.Age = 30;。自动属性:public string Name { get; set; }。迁移提示:从 Java 的 Boilerplate 转向这个,能减少 30% 代码。

C# 的创新特性

运算符重载

C# 支持运算符重载,这允许你为自定义类型定义如何使用标准运算符(如 +、-、*、/ 等)。这使得自定义类型的使用更加直观。

public class Point
{
    public int X { get; set; }
    public int Y { get; set; }
    public static Point operator +(Point p1, Point p2)
    {
        return new Point { X = p1.X + p2.X, Y = p1.Y + p2.Y };
    }
}
Point p1 = new Point { X = 1, Y = 2 };
Point p2 = new Point { X = 3, Y = 4 };
Point p3 = p1 + p2; // 使用重载的 + 运算符

在 Java 中,你不能直接重载运算符,但可以通过方法来实现类似的功能。

ndexers(索引器)

C# 的索引器允许你使用数组语法访问对象的属性,这对于集合类非常有用。

public class StringCollection
{
    private List<string> strings = new List<string>();
    public string this[int index]
    {
        get => strings[index];
        set => strings[index] = value;
    }
}
StringCollection collection = new StringCollection();
collection[0] = "Hello";
Console.WriteLine(collection[0]); // 输出: Hello

在 Java 中,你可以使用方法来实现类似的功能,但语法和使用方式有所不同。

LINQ:查询革命

C# 通过 LINQ 提供了强大的数据查询能力,它允许你以声明方式编写类型安全的查询。LINQ 可以用于数组、枚举类型以及其他集合类型。

var results = from element in array
              where element > 10
              select element;

在 Java 中,你可以使用 Stream API 来实现类似的功能,但语法和使用方式有所不同。

List<Integer> results = Arrays.stream(array)
    .filter(element -> element > 10)
    .collect(Collectors.toList());

Java 的 Stream API 也提供了类似的功能,但 C# 的 LINQ 更加直观和易于使用。

Null 合并运算符(??)

C# 提供了 Null 合并运算符(??),用于简化对可能为 null 的值的处理。

string name = nullableName ?? "Default Name";

在 Java 中,你可以使用 Optional 类来处理可能为 null 的值,但语法更为冗长。

String name = optionalName.orElse("Default Name");

异步编程:async/await

async Task<string> GetDataAsync()
{
    await Task.Delay(1000);
    return "Data";
}


调用:var data = await GetDataAsync();。Java 21+ 有 virtual threads,但语法不如 await 自然。

在 Java 中,你可以使用 CompletableFuture 来实现类似的功能,但语法和使用方式有所不同。

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "Data";
});

Java 的 virtual threads 使用方式如下:

Thread.startVirtualThread(() -> {
    String data = GetDataAsync();
    System.out.println(data);
});

函数参数传递

C# 支持按值传递和按引用传递参数。默认情况下,C# 方法参数是按值传递的,但你可以使用 refout 关键字来按引用传递参数。

public void UpdateValue(ref int value)
{
    value += 10;
}
int number = 5;
UpdateValue(ref number);
Console.WriteLine(number); // 输出: 15

在 Java 中,所有对象都是按引用传递的,但基本类型(如 int、 boolean 等)是按值传递的。Java 没有类似 C# 的 refout 关键字。

public void updateValue(int[] value) {
    value[0] += 10;
}
int[] number = {5};
updateValue(number);
System.out.println(number[0]); // 输出: 15

using 语句

C# 的 using 语句用于确保在使用完资源后自动释放它 们。它通常用于处理实现了 IDisposable 接口的对象,如文件流、数据库连接等。

using (var stream = new FileStream("file.txt", FileMode.Open))
{
    // 使用 stream
} // 自动调用 stream.Dispose()

在 Java 中,你可以使用 try-with-resources 语句来实现类似的 功能,但语法和使用方式有所不同。

try (FileInputStream stream = new FileInputStream("file.txt")) {
    // 使用 stream
} // 自动调用 stream.close()

委托(Delegates)和事件(Events)

C# 中的委托和事件提供了一种类型安全的方式来处理回调和事件通知。委托类似于 Java 中的函数接口,但 C# 的委托更为灵活。

public delegate void Notify(string message);
public class Notifier
{
    public event Notify OnNotify;

    public void Notify(string message)
    {
        OnNotify?.Invoke(message);
    }
}

在 Java 中,你可以使用函数式接口和 Lambda 表达式来实现类似的功能,但 C# 的委托和事件更为直观。

@FunctionalInterface
public interface Notify {
    void notify(String message);
}
public class Notifier {
    private Notify onNotify;
    public void setOnNotify(Notify onNotify) {
        this.onNotify = onNotify;
    }
    public void notify(String message) {
        if (onNotify != null) {
            onNotify.notify(message);
        }
    }
}

扩展方法(Extension Methods)

C# 的扩展方法允许你为现有类型添加新方法,而无需修改原始类型。这对于增强现有类的功能非常有用。

public static class StringExtensions
{
    public static string ToUpperFirst(this string str)
    {
        if (string.IsNullOrEmpty(str)) return str;
        return char.ToUpper(str[0]) + str.Substring(1);
    }
}

在 Java 中,你可以使用静态方法来实现类似的功能,但语法和使用方式有所不同。

public class StringUtils {
    public static String toUpperFirst(String str) {
        if (str == null || str.isEmpty()) return str;
        return Character.toUpperCase(str.charAt(0)) + str.substring(1);
    }
}

struct 值类型

C# 中的 struct 是一种值类型,允许你定义轻量级的数据结构。与 Java 的类不同,struct 是按值传递的,这意味着它们在赋值时会复制数据,而不是引用。

public struct Point
{
    public int X { get; set; }
    public int Y { get; set; }
}
Point p1 = new Point { X = 10, Y = 20 };
Point p2 = p1; // 复制数据
p2.X = 30; // 修改 p2 不影响 p1
Console.WriteLine(p1.X); // 输出: 10

在 Java 中,所有类都是引用类型,Java 没有类似 C# 的 struct。Java 中的类是按引用传递的,这意味着当你将一个对象赋值给另一个变量时,实际上是复制了对象的引用,而不是对象本身。

public class Point {
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }
}
Point p1 = new Point(10, 20);
Point p2 = p1; // 复制引用
p2.setX(30); // 修改 p2 也会影响 p1
System.out.println(p1.getX()); // 输出: 30

记录类型(Record Types)

C# 9 引入了记录类型(Record Types),它们是不可变的引用类型,主要用于存储数据。记录类型提供了内置的值相等性比较和简化的语法。

public record Person(string Name, int Age);
var person = new Person("Alice", 30);
Console.WriteLine(person.Name); // 输出: Alice

在 Java 中,你可以使用 record 关键字来定义类似的不可变数据 类型,但 C# 的记录类型提供了更多的功能和简化的语法。

public record Person(String name, int age) {}
Person person = new Person("Alice", 30);
System.out.println(person.name()); // 输出: Alice

运算符

可空

C# 支持可空类型(Nullable Types),允许值类型(如 int、bool 等)可以为 null。这在处理数据库或其他可能缺失数据的场景时非常有用。

int? nullableInt = null;
if (nullableInt.HasValue)
{
    Console.WriteLine(nullableInt.Value);
}

在 Java 中,你可以使用 Optional 类来处理可能为 null 的值,但语法更为冗长。

Optional<Integer> optionalInt = Optional.empty();
if (optionalInt.isPresent()) {
    System.out.println(optionalInt.get());

}

?? 运算符

C# 的 Null 合并运算符(??)允许你在一个表达式中提供默认值,如果左侧的值为 null,则返回右侧的值。

string name = nullableName ?? "Default Name";

在 Java 中,你可以使用 Optional 类来处理可能为 null 的值,但语 法更为冗长。

String name = optionalName.orElse("Default Name");

.? 运算符

C# 的 Null 条件运算符(?.)允许你在访问对象的属性 或方法时,如果对象为 null,则返回 null,而不是抛出 NullReferenceException。

string name = person?.Name;

在 Java 中,你可以使用 Optional 类来处理可能为 null 的值,但语 法更为冗长。

String name = person != null ? person.getName() : null;

空合并赋值运算符(??=)

C# 的空合并赋值运算符(??=)允许你在变量为 null 时为其赋值。

string name = null;
name ??= "Default Name"; // 如果 name 为 null,则赋值为 "Default Name

在 Java 中,你可以使用 Optional 类来处理可能为 null 的值,但语 法更为冗长。

String name = null;
if (name == null) {
    name = "Default Name"; // 如果 name 为 null,则赋值为 "Default Name
}

委托合并运算符(+= 和 -=)

C# 的委托合并运算符(+= 和 -=)允许你将多个方法添加到同一个委托中,或者从委托中移除方法。这使得事件处理和回调更加灵活。

public delegate void Notify(string message);
public class Notifier
{
    public event Notify OnNotify;
    public void Notify(string message)
    {
        OnNotify?.Invoke(message);
    }
}
Notifier notifier = new Notifier();
notifier.OnNotify += message => Console.WriteLine("Received: " + message);
notifier.Notify("Hello, World!"); // 输出: Received: Hello, World!
notifier.OnNotify -= message => Console.WriteLine("Received: " + message);

在 Java 中,你可以使用函数式接口和 Lambda 表达式来实现类似的功能,但 C# 的委托合并运算符更为直观。

@FunctionalInterface
public interface Notify {
    void notify(String message);
}
public class Notifier {
    private Notify onNotify;
    public void setOnNotify(Notify onNotify) {
        this.onNotify = onNotify;
    }
    public void notify(String message) {
        if (onNotify != null) {
            onNotify.notify(message);
        }
    }
}
Notifier notifier = new Notifier();
notifier.setOnNotify(message -> System.out.println("Received: " + message));
notifier.notify("Hello, World!"); // 输出: Received: Hello, World!
notifier.setOnNotify(null); // 移除通知

指针

C# 支持指针,但默认情况下是禁用的。你需要在项目文件中启用不安全代码(unsafe code)才能使用指针。

unsafe
{
    int* p = stackalloc int[10]; // 分配一个整数数组
    for (int i = 0; i < 10; i++)
    {
        p[i] = i;
    }
    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine(p[i]); // 输出: 0 1 2 3
    }
}

在 Java 中,你不能直接使用指针,但可以使用 Unsafe 类来实现类似 的功能,但这需要额外的配置和权限。

import sun.misc.Unsafe;
Unsafe unsafe = Unsafe.getUnsafe();
long address = unsafe.allocateMemory(10 * Integer.BYTES); // 分配一个整数
for (int i = 0; i < 10; i++) {
    unsafe.putInt(address + i * Integer.BYTES, i);
}
for (int i = 0; i < 10; i++) {
    System.out.println(unsafe.getInt(address + i * Integer.BYTES)); // 输出: 0 1 2 3
}
unsafe.freeMemory(address); // 释放内存

总结

C# 和 Java 在语法和特性上有很多相似之处,但 C# 提供了一些独特的功能,如属性、委托、LINQ 等,这些功能可以使代码更简洁、更易读。 对于 Java 开发者来说,学习 C# 可以帮助你更好地理解面向对象编程的概念,并掌握一些新的编程范式。 希望这篇文章能帮助你快速入门 C#,并在实际项目中应用这些知识。