1. C#语言基础
1.1 C#语法概览
欢迎来到C#的世界!对于刚从Java转过来的开发者来说,你会发现C#和Java有很多相似之处,但C#也有其独特的魅力和强大之处。让我们一起来探索C#的基本语法,并比较一下与Java的异同。
程序结构
C#程序的基本结构与Java非常相似。这里是一个简单的C#程序:
using System;
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
}
对比Java的版本:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
你会发现,两者的结构非常相似。主要的区别在于:
- C#使用
using
关键字导入命名空间,而Java使用import
。 - C#的
Main
方法是static void Main(string[] args)
,而Java是public static void main(String[] args)
。 - C#使用
Console.WriteLine()
输出,Java使用System.out.println()
。
在c# 9的最新语法上还可以更简洁,是的没错,只需要一行代码,不需要写命名空间,类,方法,直接编写代码,当然这个方式只存在c#9以上的版本。
Console.WriteLine("Hello, World!");
命名约定
C#和Java的命名约定有些许不同:
- C#中,方法名和属性名通常使用PascalCase(如
CalculateTotal
)。 - 局部变量和参数使用camelCase(如
totalAmount
)。 - 接口名称以"I"开头(如
IDisposable
)。
而Java中:
- 方法名和变量名都使用camelCase。
- 接口名称不需要特殊前缀。
数据类型
C#和Java的基本数据类型很相似,但也有一些区别:
C#:
int x = 10;
long y = 100L;
float f = 3.14f;
double d = 3.14;
decimal m = 100.50m;
bool isTrue = true;
char c = 'A';
string s = "Hello";
Java:
int x = 10;
long y = 100L;
float f = 3.14f;
double d = 3.14;
boolean isTrue = true;
char c = 'A';
String s = "Hello";
注意C#特有的decimal
类型,它提供了更高精度的小数计算,特别适合金融相关的应用。
数组
C#和Java的数组声明稍有不同:
C#:
int[] numbers = new int[5];
string[] names = { "Alice", "Bob", "Charlie" };
Java:
int[] numbers = new int[5];
String[] names = { "Alice", "Bob", "Charlie" };
控制结构
C#和Java的控制结构几乎完全相同:
// if语句
if (condition)
{
// code
}
else if (anotherCondition)
{
// code
}
else
{
// code
}
// for循环
for (int i = 0; i < 10; i++)
{
// code
}
// while循环
while (condition)
{
// code
}
// switch语句
switch (variable)
{
case value1:
// code
break;
case value2:
// code
break;
default:
// code
break;
}
这些结构在Java中的写法完全相同。
异常处理
C#和Java的异常处理也非常相似:
C#:
try
{
// 可能抛出异常的代码
}
catch (SpecificException ex)
{
// 处理特定异常
}
catch (Exception ex)
{
// 处理一般异常
}
finally
{
// 总是要执行的代码
}
Java的异常处理结构完全相同。
注释
C#和Java的注释方式也是一样的:
// 这是单行注释
/*
* 这是多行注释
*/
/// <summary>
/// 这是XML文档注释,类似于Java的Javadoc
/// </summary>
小结
通过这个概览,你可以看到C#和Java在语法上有很多相似之处。这意味着作为一个Java开发者,你可以相对轻松地过渡到C#。然而,C#也有其独特的特性和语法糖,使得某些任务更加简洁和高效。
在接下来的章节中,我们将深入探讨C#的各个方面,包括它独特的特性如属性、事件、委托等。这些概念可能对Java开发者来说比较新,但它们是C#强大功能的关键所在。记住,学习一门新的语言不仅是学习语法,更是学习一种新的思维方式。让我们继续我们的C#学习之旅吧!
1.2 变量和数据类型
在C#中,变量和数据类型是编程的基础。对于从Java转过来的开发者来说,你会发现很多熟悉的概念,但C#也有一些独特的特性。让我们深入探讨C#的变量和数据类型,并与Java进行比较。
变量声明
C#和Java的变量声明方式非常相似:
C#:
int age = 25;
string name = "Alice";
bool isStudent = true;
Java:
int age = 25;
String name = "Alice";
boolean isStudent = true;
主要区别在于:
- C#使用
string
(小写),而Java使用String
(大写)。 - C#使用
bool
,而Java使用boolean
。
基本数据类型
C#和Java都有类似的基本数据类型,但C#提供了更多的选择:
C# 类型 | Java 类型 | 大小 | 范围 |
---|---|---|---|
sbyte | byte | 8位 | -128 到 127 |
byte | - | 8位 | 0 到 255 |
short | short | 16位 | -32,768 到 32,767 |
ushort | - | 16位 | 0 到 65,535 |
int | int | 32位 | -2^31 到 2^31-1 |
uint | - | 32位 | 0 到 2^32-1 |
long | long | 64位 | -2^63 到 2^63-1 |
ulong | - | 64位 | 0 到 2^64-1 |
float | float | 32位 | ±1.5x 10^-45 到 ±3.4 x 10^38 |
double | double | 64位 | ±5.0 × 10^-324 到 ±1.7 × 10^308 |
decimal | - | 128位 | ±1.0 x 10^-28 到 ±7.9 x 10^28 |
char | char | 16位 | U+0000 到 U+FFFF |
bool | boolean | 8位 | true或 false |
注意C#提供了无符号整数类型(byte
, ushort
, uint
, ulong
)和decimal
类型,这些在Java中是没有的。
值类型和引用类型
C#和Java都区分值类型和引用类型,但C#的处理更加灵活:
-
值类型(Value Types):
- 在C#中,所有的基本数据类型(int, float, bool等)和struct都是值类型。
- 值类型直接存储它们的数据。
-
引用类型(Reference Types):
- 类(class)、接口(interface)、委托(delegate)和数组(array)是引用类型。
- 引用类型存储对其数据(对象)的引用。
C#独特之处:
- C#允许使用
struct
关键字创建自定义值类型。 - C#的
string
虽然是引用类型,但具有值类型的一些特性(如不可变性)。
可空类型
C#引入了可空类型的概念,这在Java中是没有的:
int? nullableInt = null;
bool? nullableBool = null;
可空类型允许值类型也可以赋值为null
,这在处理数据库或用户输入时非常有用。
var关键字
C#提供了var
关键字用于隐式类型声明:
var x = 10; // 编译器推断x为int类型
var name = "Alice"; // 编译器推断name为string类型
Java从Java 10开始引入了类似的var
关键字,但使用范围更受限制。
常量
C#使用const
关键字声明常量:
const int MaxValue = 100;
const string AppName = "MyApp";
Java使用final
关键字:
final int MAX_VALUE = 100;
final String APP_NAME = "MyApp";
枚举
C#和Java都支持枚举,但C#的枚举更加灵活:
C#:
enum Days
{
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
}
// 可以指定底层类型和值
enum Status : byte
{
Active = 1,
Inactive = 0,Suspended = 2
}
Java:
enum Days {
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
}
C#的枚举可以指定底层类型,而Java的枚举实际上是特殊的类。
类型转换
C#提供了多种类型转换方法:
-
隐式转换:
int x = 10; long y = x; // 隐式转换,不需要显式转换
-
显式转换(强制类型转换):
doubled = 3.14; int i = (int)d; // 显式转换,可能会损失精度
-
使用Convert类:
string s = "123"; int i = Convert.ToInt32(s);
-
使用Parse方法:
string s = "3.14"; double d = double.Parse(s);
-
TryParse方法(安全转换):
string s = "123"; int result; if (int.TryParse(s, out result)) { Console.WriteLine($"Converted value: {result}"); } else { Console.WriteLine("Conversion failed"); }
Java的类型转换相对简单一些,主要依赖于强制类型转换和包装类的方法。
小结
虽然C#和Java在变量和数据类型方面有很多相似之处,但C#提供了更多的选择和灵活性。C#的可空类型、更丰富的基本数据类型、更灵活的枚举和方便的类型转换方法,都为开发者提供了更多的工具来处理各种数据场景。
作为一个从Java转向C#的开发者,你会发现这些额外的特性可以让你的代码更加简洁和表达力更强。在实际编程中,合理利用这些特性可以提高代码的可读性和性能。
在接下来的学习中,我们将深入探讨C#的更多高级特性,如属性、索引器、泛型等。这些概念将进一步展示C#相对于Java的独特优势。继续保持学习的热情,你会发现C#是一个功能丰富、富有表现力的语言!
1.3 运算符和表达式
C#的运算符和表达式与Java有很多相似之处,但也有一些独特的特性。让我们深入了解C#的运算符和表达式,并与Java进行比较。
算术运算符
C#和Java的算术运算符基本相同:
- 加法 (+)
- 减法 (-)
- 乘法 (*)
- 除法 (/)
- 取模 (%)
示例:
int a = 10, b = 3;
int sum = a + b; // 13
int difference = a - b; // 7
int product = a * b; // 30
int quotient = a / b; // 3 (整数除法)
int remainder = a % b; // 1
注意:C#和Java在整数除法时都会舍去小数部分,如果要得到精确结果,至少有一个操作数应该是浮点数。
赋值运算符
C#和Java的赋值运算符也基本相同:
- 简单赋值 (=)
- 复合赋值 (+=, -=, *=, /=, %=)
C#特有的复合赋值运算符:
- ??= (空合并赋值运算符,C# 8.0引入)
示例:
int x = 5;
x += 3; // 等同于 x = x + 3
x -= 2; // 等同于 x = x - 2
string name = null;
name ??= "John"; // 如果name为null,赋值为"John"
比较运算符
C#和Java的比较运算符完全相同:
- 等于 (==)
- 不等于 (!=)
- 大于 (>)
- 小于 (<)
- 大于等于 (>=)
- 小于等于 (<=)
示例:
int a = 5, b = 7;
bool isEqual = (a == b);// false
bool isNotEqual = (a !=b); // true
bool isGreater = (a > b);// false
bool isLess = (a < b); // true
bool isGreaterOrEqual = (a >= b); // false
bool isLessOrEqual = (a <= b);// true
逻辑运算符
C#和Java的逻辑运算符也是相同的:
- 逻辑与 (&&)
- 逻辑或 (||)
- 逻辑非 (!)
示例:
bool a = true, b = false;
bool andResult = a && b; // false
bool orResult = a || b; // true
bool notResult = !a; // false
位运算符
C#和Java的位运算符也基本相同:
- 按位与 (&)
- 按位或 (|)
- 按位异或 (^)
- 按位取反 (~)
- 左移 (<<)
- 右移 (>>)
C#特有的位运算符:
- 无符号右移 (>>>)
示例:
int a = 60;// 二进制: 0011 1100
int b = 13; // 二进制: 0000 1101
int c = a & b; // 12(二进制: 0000 1100)
int d = a | b; // 61 (二进制: 0011 1101)
int e = a ^ b; // 49 (二进制: 0011 0001)
int f = ~a; // -61 (二进制: 1100 0011, 补码表示)
int g = a << 2; // 240 (二进制: 1111 0000)
int h = a >> 2; // 15 (二进制: 0000 1111)
条件运算符
C#和Java都有三元条件运算符:
int a = 10, b = 20;
int max = (a > b) ? a : b; // 20
C#特有的条件运算符:
- 空合并运算符 (??)
- 空条件运算符(?.)
示例:
string name = null;
string displayName = name ?? "Guest"; // "Guest"
class Person
{
public string Name { get; set; }
}
Person person = null;
int? nameLength = person?.Name?.Length; // null
类型测试运算符
C#提供了一些Java中没有的类型测试运算符:
- is 运算符:检查对象是否与特定类型兼容
- as 运算符:执行类型转换,如果转换失败,返回null
示例:
object obj = "Hello";
if (obj is string)
{
Console.WriteLine("obj is a string");
}
string str = obj as string;
if (str != null)
{
Console.WriteLine($"The string is: {str}");
}
Lambda 表达式
C#和Java都支持Lambda表达式,但语法略有不同:
C#:
Func<int, int> square = x => x * x;
int result = square(5); // 25
Java:
Function<Integer, Integer> square = x -> x * x;
int result = square.apply(5); // 25
空合并运算符(??)
C#特有的空合并运算符可以简化处理可能为null的情况:
string name = null;
string displayName = name ?? "Guest"; // "Guest"
在Java中,你可能需要这样写:
String name = null;
String displayName = (name != null) ? name : "Guest";
表达式体成员 (Expression-bodied members)
C#允许使用更简洁的语法来定义属性和方法:
public class Circle
{
public double Radius { get; set; }
public double Diameter => Radius * 2;
public double CalculateArea() => Math.PI * Radius * Radius;
}
这种语法在Java中是不存在的。
字符串插值
C#提供了非常方便的字符串插值语法:
string name = "Alice";
int age = 30;
string message = $"My name is {name} and I am {age} years old.";
Java在较新的版本中也引入了类似的功能,但语法不同:
String name = "Alice";
int age = 30;
String message = String.format("My name is %s and I am %d years old.", name, age);
小结
虽然C#和Java在运算符和表达式方面有很多相似之处,但C#提供了一些额外的特性,如空合并运算符、空条件运算符、表达式体成员等,这些可以让代码更加简洁和表达力更强。
作为一个从Java转向C#的开发者,你会发现这些额外的特性可以让你的代码更加优雅和易读。在实际编程中,合理利用这些特性可以提高代码质量和开发效率。
在接下来的学习中,我们将深入探讨C#的更多高级特性,如LINQ、异步编程等。这些概念将进一步展示C#相对于Java的独特优势。继续保持学习的热情,你会发现C#是一个功能丰富、表达力强的语言!
1.4 控制流语句
控制流语句是编程语言的基本构建块,用于控制程序的执行路径。C#和Java在这方面非常相似,但C#也有一些独特的特性。让我们深入了解C#的控制流语句,并与Java进行比较。
if-else 语句
C#和Java的if-else语句几乎完全相同:
int x = 10;
if (x > 5)
{
Console.WriteLine("x is greater than 5");
}
else if (x < 5)
{
Console.WriteLine("x is less than 5");
}
else
{
Console.WriteLine("x is equal to 5");
}
C#特有的特性:
- 可空类型的使用:
int? x = null;
if (x.HasValue)
{
Console.WriteLine($"x has a value: {x.Value}");
}
else
{
Console.WriteLine("x is null");
}
- 模式匹配(C# 7.0+):
object obj = "Hello";
if (obj is string s)
{
Console.WriteLine($"The string is: {s}");
}
switch 语句
C#的switch语句比Java的更加灵活:
int day = 3;
switch (day)
{
case 1:
Console.WriteLine("Monday");
break;
case 2:
Console.WriteLine("Tuesday");
break;
case 3:
case 4:
case 5:
Console.WriteLine("Midweek");
break;
default:
Console.WriteLine("Weekend");
break;
}
C#特有的特性:
- 模式匹配(C# 7.0+):
object obj = 123;
switch (obj)
{
case int i when i > 100:
Console.WriteLine($"Large integer: {i}");
break;
case string s:
Console.WriteLine($"String value: {s}");
break;
case null:
Console.WriteLine("Null value");
break;
default:
Console.WriteLine("Unknown type");
break;
}
- switch 表达式(C# 8.0+):
string GetDayType(int day) => day switch
{
1 => "Monday",
2 => "Tuesday",
3 or 4 or 5 => "Midweek",
_ => "Weekend"
};
循环语句
C#和Java的循环语句非常相似:
- for循环:
for (int i = 0; i < 5; i++)
{
Console.WriteLine(i);
}
- while 循环:
int i = 0;
while (i < 5)
{
Console.WriteLine(i);
i++;
}
- do-while 循环:
int i = 0;
do
{
Console.WriteLine(i);
i++;
} while (i < 5);
- foreach 循环:
string[] fruits = { "apple", "banana", "cherry" };
foreach (string fruit in fruits)
{
Console.WriteLine(fruit);
}
C#特有的特性:
- LINQ与foreach的结合:
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
foreach (var num in numbers.Where(n => n % 2 == 0))
{
Console.WriteLine(num);
}
跳转语句
C#和Java都支持以下跳转语句:
- break:跳出当前循环或switch语句
- continue:跳过当前循环的剩余部分,开始下一次迭代
- return:从方法中返回,并可选择返回一个值
C#特有的跳转语句:
- goto:虽然不推荐使用,但C#保留了goto语句
int i = 0;
start:
if (i < 5)
{
Console.WriteLine(i);
i++;
goto start;
}
异常处理
C#和Java的异常处理机制非常相似:
try
{
int result = 10 / 0;
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"Division by zero error: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
}
finally
{
Console.WriteLine("This always executes");
}
C#特有的特性:
- 异常过滤器(C# 6.0+):
try
{
// 可能抛出异常的代码
}
catch (Exception ex) when (ex.InnerException != null)
{
Console.WriteLine($"Inner exception: {ex.InnerException.Message}");
}
- using 语句(简化资源管理):
using (var file = new System.IO.StreamReader("file.txt"))
{
string content = file.ReadToEnd();
Console.WriteLine(content);
}
// file自动关闭
- using 声明(C# 8.0+):
using var file = new System.IO.StreamReader("file.txt");
string content = file.ReadToEnd();
Console.WriteLine(content);
// file 在作用域结束时自动关闭
小结
虽然C#和Java在控制流语句方面有很多相似之处,但C#提供了一些额外的特性,如模式匹配、switch表达式、异常过滤器等,这些可以让代码更加简洁和表达力更强。
作为一个从Java转向C#的开发者,你会发现这些额外的特性可以让你的代码更加优雅和易读。特别是模式匹配和switch表达式,它们可以大大简化复杂的条件逻辑。
在实际编程中,合理利用这些特性可以提高代码质量和开发效率。例如,使用模式匹配可以使类型检查和转换更加简洁,使用switch表达式可以使复杂的条件判断更加清晰。
在接下来的学习中,我们将深入探讨C#的更多高级特性,如LINQ、异步编程等。这些概念将进一步展示C#相对于Java的独特优势。继续保持学习的热情,你会发现C#是一个功能丰富、表达力强的语言!
1.5 方法和参数
方法(在Java中称为函数)是编程中最基本的代码组织单元。C#和Java在方法定义和使用上有很多相似之处,但C#提供了一些额外的特性,使得方法定义和调用更加灵活。让我们深入探讨C#的方法和参数,并与Java进行比较。
方法定义
C#和Java的基本方法定义非常相似:
public int Add(int a, int b)
{
return a + b;
}
Java中的等效代码:
public int add(int a, int b) {
return a + b;
}
主要区别:
- C#方法名通常使用PascalCase,而Java使用camelCase。
- C#支持方法重载,Java也支持。
参数传递
C#和Java都支持值传递和引用传递,但C#提供了更多选项:
- 值参数(默认):
public void IncrementValue(int x)
{
x++; // 不影响原始值
}
- 引用参数(ref 关键字):
public void IncrementRef(ref int x)
{
x++; // 修改原始值
}
// 调用
int num = 5;
IncrementRef(ref num);
Console.WriteLine(num); // 输出 6
Java没有直接等效的引用参数,但可以通过包装类或数组实现类似效果。
- 输出参数(out 关键字):
public bool TryParse(string s, out int result)
{
return int.TryParse(s, out result);
}
// 调用
if (TryParse("123", out int number))
{
Console.WriteLine($"Parsed number: {number}");
}
Java没有直接等效的输出参数。
- 参数数组(params 关键字):
public int Sum(params int[] numbers)
{
return numbers.Sum();
}
// 调用
int total = Sum(1, 2, 3, 4, 5);
Java使用可变参数(varargs)实现类似功能:
public int sum(int... numbers) {
return Arrays.stream(numbers).sum();
}
方法重载
C#和Java都支持方法重载,允许在同一个类中定义多个同名但参数列表不同的方法:
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
public double Add(double a, double b)
{
return a + b;
}
}
Java的方法重载与C#基本相同。
可选参数
C#支持可选参数,这在Java中直到最近才引入:
public void Greet(string name, string greeting = "Hello")
{
Console.WriteLine($"{greeting}, {name}!");
}
// 调用
Greet("Alice"); // 输出: Hello, Alice!
Greet("Bob", "Hi"); // 输出: Hi, Bob!
在Java中,你通常需要使用方法重载来实现类似功能:
public void greet(String name) {
greet(name, "Hello");
}
public void greet(String name, String greeting) {
System.out.println(greeting + ", " + name + "!");
}
命名参数
C#支持命名参数,可以提高代码的可读性:
public void CreateUser(string name, int age, bool isAdmin = false)
{
//方法实现
}
// 调用
CreateUser(name: "Alice", age: 30, isAdmin: true);
CreateUser(age: 25, name: "Bob"); // 可以改变参数顺序
Java不支持命名参数,但可以使用建造者模式来实现类似的效果。
表达式体方法
C# 6.0引入了表达式体方法,可以使简单方法的定义更加简洁:
public int Add(int a, int b) => a + b;
public string GetFullName(string firstName, string lastName) => $"{firstName} {lastName}";
Java不支持这种语法糖。
本地函数
C# 7.0引入了本地函数,允许在方法内定义函数:
public int Factorial(int n)
{
int LocalFactorial(int x)
{
return x <= 1 ? 1 : x * LocalFactorial(x - 1);
}
return LocalFactorial(n);
}
Java不直接支持本地函数,但可以使用匿名内部类或lambda表达式来实现类似功能。
异步方法
C#对异步编程的支持非常强大,使用async和await关键字:
public async Task<string> FetchDataAsync(string url)
{
using var client = new HttpClient();
return await client.GetStringAsync(url);
}
// 调用
string data = await FetchDataAsync("https://api.example.com");
Java也支持异步编程,但语法和使用方式与C#不同,通常使用CompletableFuture:
public CompletableFuture<String> fetchDataAsync(String url) {
return CompletableFuture.supplyAsync(() -> {
// 使用HttpClient获取数据
return "data";
});
}
// 调用
String data = fetchDataAsync("https://api.example.com").join();
扩展方法
C#允许你为现有类型添加新方法,而不需要修改原始类型的定义:
public static class StringExtensions
{
public static bool IsNullOrEmpty(this string str)
{
return string.IsNullOrEmpty(str);
}
}
// 使用
string name = "Alice";
bool isEmpty = name.IsNullOrEmpty();
Java不支持扩展方法,但可以使用静态工具类来实现类似功能。
泛型方法
C#和Java都支持泛型方法,允许你编写可以处理多种类型的方法:
public T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
// 使用
int maxInt = Max(5, 10);
string maxString = Max("apple", "banana");
Java的泛型方法语法略有不同:
public <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
方法组合与函数式编程
C#对函数式编程有很好的支持,可以轻松组合和传递方法:
Func<int, int> square = x => x * x;
Func<int, int> addOne = x => x + 1;
Func<int, int> squareThenAddOne = x => addOne(square(x));
int result = squareThenAddOne(5); // 26
Java也支持函数式编程,但语法略有不同:
Function<Integer, Integer> square = x -> x * x;
Function<Integer, Integer> addOne = x -> x + 1;
Function<Integer, Integer> squareThenAddOne = square.andThen(addOne);
int result = squareThenAddOne.apply(5); // 26
小结
虽然C#和Java在方法和参数的基本概念上很相似,但C#提供了更多的特性和灵活性。C#的引用参数、输出参数、命名参数、可选参数等特性可以让方法定义和调用更加灵活和清晰。此外,C#的异步方法、扩展方法和表达式体方法等特性可以让代码更加简洁和易读。
作为一个从Java转向C#的开发者,你会发现这些额外的特性可以大大提高你的编程效率和代码质量。例如,命名参数和可选参数可以减少方法重载的需求,扩展方法可以让你更容易地扩展现有类型的功能,而async/await则可以大大简化异步编程的复杂性。
在实际编程中,合理利用这些特性可以让你的代码更加清晰、简洁和易于维护。例如,使用命名参数可以提高代码的可读性,使用扩展方法可以使你的代码更加模块化,而使用异步方法可以提高应用程序的响应性。
随着你对C#的深入学习,你会发现更多强大的特性和用法。保持学习和实践的热情,你将能够充分利用C#的强大功能,成为一个高效的.NET开发者!
1.6 类和对象
类和对象是面向对象编程的核心概念,C#和Java在这方面有很多相似之处,但C#提供了一些额外的特性和语法糖,使得类的定义和使用更加灵活和简洁。让我们深入探讨C#的类和对象,并与Java进行比较。
类的定义
C#和Java的基本类定义非常相似:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public void SayHello()
{
Console.WriteLine($"Hello, my name is {Name} and I'm {Age} years old.");
}
}
Java中的等效代码:
public class Person {
private String name;
private int age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public void sayHello() {
System.out.println("Hello, my name is " + name + " and I'm " + age + " years old.");
}
}
主要区别:
- C#使用属性(Properties)代替了Java的getter和setter方法。
- C#的方法名通常使用PascalCase,而Java使用camelCase。
构造函数
C#和Java的构造函数定义类似:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
C#特有的特性:
- 构造函数初始化器:
public class Employee : Person
{
public string Company { get; set; }
public Employee(string name, int age, string company) : base(name, age)
{
Company = company;
}
}
- 主构造函数(C# 9.0+):
public class Person(string name, int age)
{
public string Name { get; set; } = name;
public int Age { get; set; } = age;
}
属性
C#的属性是一个强大的特性,可以替代Java中的getter和setter方法:
public class Person
{
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
// 自动实现的属性
public int Age { get; set; }
// 只读属性
public bool IsAdult => Age >= 18;
}
C# 6.0+引入了更简洁的属性语法:
public class Person
{
public string Name { get; set; } = "John Doe";
public int Age { get; set; }
public bool IsAdult => Age >= 18;
}
静态成员
C#和Java都支持静态成员:
public class MathHelper
{
public static double PI = 3.14159;
public static int Add(int a, int b)
{
return a + b;
}
}
// 使用
double pi = MathHelper.PI;
int sum = MathHelper.Add(5, 3);
继承
C#和Java的继承语法略有不同:
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("The animal makes a sound");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("The dog barks");
}
}
Java中的等效代码:
public class Animal {
public void makeSound() {
System.out.println("The animal makes a sound");
}
}
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("The dog barks");
}
}
主要区别:
- C#使用冒号(
:
)表示继承,Java使用extends
关键字。 - C#需要使用
virtual
和override
关键字来实现方法重写,Java只需要使用@Override
注解。
接口
C#和Java的接口定义类似,但C#允许接口包含默认实现(C# 8.0+):
public interface IAnimal
{
void MakeSound();
void Move() => Console.WriteLine("The animal moves");
}
public class Dog : IAnimal
{
public void MakeSound()
{
Console.WriteLine("The dog barks");
}
// Move方法使用接口的默认实现
}
Java8+也支持接口默认方法:
public interface Animal {
void makeSound();
default void move() {
System.out.println("The animal moves");
}
}
匿名类型
C#支持匿名类型,可以快速创建简单的对象:
var person = new { Name = "Alice", Age = 30 };
Console.WriteLine($"{person.Name} is {person.Age} years old");
Java也支持匿名类,但主要用于创建接口或抽象类的匿名实现。
Record类型(C# 9.0+)
C# 9.0引入了Record类型,用于创建不可变的引用类型:
public record Person(string Name, int Age);
var alice = new Person("Alice", 30);
var bob = alice with { Name = "Bob" }; // 创建一个新记录,只修改Name
Java 14+引入了类似的Record特性:
public record Person(String name, int age) {}
对象初始化器
C#支持对象初始化器,可以在创建对象时直接设置属性:
var person = new Person
{
Name = "Alice",
Age = 30
};
Java不直接支持这种语法,通常使用建造者模式来实现类似效果。
扩展方法
C#允许为现有类型添加新方法,而不需要修改原始类型:
public static class StringExtensions
{
public static bool IsNullOrEmpty(this string str)
{
return string.IsNullOrEmpty(str);
}
}
// 使用
string name = "Alice";
bool isEmpty = name.IsNullOrEmpty();
Java不支持扩展方法,但可以使用静态工具类来实现类似功能,或者使用manifold 插件支持。
部分类(Partial Classes)
C#支持部分类,允许将一个类的定义分散到多个文件中:
// File1.cs
public partial class MyClass
{
public void Method1() { }
}
// File2.cs
public partial class MyClass
{
public void Method2() { }
}
Java不支持部分类的概念。
索引器(Indexers)
C#支持索引器,允许类像数组一样通过索引访问:
public class StringCollection
{
private List<string> items = new List<string>();
public string this[int index]
{
get { return items[index]; }
set { items[index] = value; }
}
}
// 使用
var collection = new StringCollection();
collection[0] = "Hello";
Console.WriteLine(collection[0]); // 输出: Hello
Java没有直接等效的特性,通常需要定义专门的get和set方法。
运算符重载
C#允许为自定义类型定义运算符的行为:
public struct Complex
{
public double Real { get; set; }
public double Imaginary { get; set; }
public static Complex operator +(Complex c1, Complex c2)
{
return new Complex
{
Real = c1.Real + c2.Real,Imaginary = c1.Imaginary + c2.Imaginary
};
}
}
// 使用
var c1 = new Complex { Real = 1, Imaginary = 2 };
var c2 = new Complex { Real = 3, Imaginary = 4 };
var result = c1 + c2;
Java不支持运算符重载。
嵌套类型
C#和Java都支持嵌套类型,但C#的访问规则更加灵活:
public class OuterClass
{
private int outerField = 10;
public class InnerClass
{
public void AccessOuterField(OuterClass outer)
{
Console.WriteLine(outer.outerField);
}
}
}
在C#中,嵌套类可以访问外部类的私有成员,而在Java中,内部类需要外部类的实例才能访问其私有成员。
密封类和方法
C#使用sealed
关键字来防止类被继承或方法被重写:
public sealed class FinalClass
{
// 这个类不能被继承
}
public class BaseClass
{
public virtual void VirtualMethod() { }
}
public class DerivedClass : BaseClass
{
public sealed override void VirtualMethod() { }
// 这个方法不能在子类中被重写
}
Java使用final
关键字实现类似功能。
析构函数和终结器
C#支持析构函数(用于结构体)和终结器(用于类):
public class ResourceHolder
{
private IntPtr resource;
public ResourceHolder()
{
resource = AllocateResource();
}
~ResourceHolder()
{
FreeResource(resource);
}
private IntPtr AllocateResource() { /*分配资源 */ }
private void FreeResource(IntPtr handle) { /* 释放资源 */ }
}
Java不支持析构函数,但有类似的finalize()
方法(虽然不推荐使用)。
属性访问器的可访问性
C#允许为属性的getter 和 setter 单独设置访问级别:
public class Person
{
public string Name { get; private set; }
public Person(string name)
{
Name = name;
}
}
Java不支持这种细粒度的访问控制。
init only setters(C#9.0+)
C#9.0引入了init only setters
,允许在对象初始化时设置属性值,而之后这些属性是不可变的:
var circle = new Circle { Radius = 5 };
var bigCircle = new Circle { Diameter = 20 };
public class Circle
{
private double _radius;
public double Radius
{
get => _radius;
init => _radius = value;
}
public double Diameter
{
get => 2 * _radius;
init => _radius = value / 2;
}
}
Java没有直接等效的特性。
顶级语句(C# 9.0+)
从C# 9.0开始,可以在文件级别直接编写代码,而不需要显式的Main方法:
Console.WriteLine("Hello, World!");
这个特性简化了小型程序和脚本的编写。Java仍然需要一个包含main方法的类。
模式匹配(C# 7.0+)
C#支持高级的模式匹配,可以在switch语句和is表达式中使用:
object obj = "Hello";
if (obj is string s && s.Length > 5)
{
Console.WriteLine($"It's a long string: {s}");
}
var result = obj switch
{
string s => $"It's a string: {s}",
int i => $"It's an int: {i}",
_ => "It's something else"
};
Java支持有限形式的模式匹配(从Java 14开始),但不如C#灵活。
小结
C#提供了丰富的特性来定义和使用类和对象,许多这些特性在Java中是没有直接等价物的。这些特性不仅可以让代码更加简洁和表达力更强,还可以提高开发效率和代码质量。
作为一个从Java转向C#的开发者,你会发现这些额外的特性可以让你以新的方式思考和组织代码。例如,索引器可以让你的自定义类型像数组一样使用,运算符重载可以让你的类型更自然地参与数学运算,而模式匹配则可以简化复杂的类型检查和转换逻辑。
在实际编程中,合理利用这些特性可以大大提高代码的可读性和可维护性。例如,使用属性可以简化数据封装,使用记录类型可以简化不可变数据模型的创建,而使用模式匹配可以使复杂的条件逻辑更加清晰。
随着你对C#的深入学习和实践,你会发现更多强大的特性和用法。保持学习和实践的热情,你将能够充分利用C#的强大功能,成为一个高效的.NET开发者!记住,编程语言只是工具,关键是要理解背后的概念和原理,并能够在实际问题中灵活应用这些知识。
1.7 继承和多态
继承和多态是面向对象编程的核心概念,C#和Java在这方面有许多相似之处,但C#提供了一些额外的特性和语法,使得继承和多态的实现更加灵活和强大。让我们深入探讨C#的继承和多态,并与Java进行比较。
基本继承
C#和Java的基本继承语法略有不同:
C#:
public class Animal
{
public string Name { get; set; }
public virtual void MakeSound()
{
Console.WriteLine("The animal makes a sound");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("The dog barks");
}
}
Java:
public class Animal {
private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public void makeSound() {
System.out.println("The animal makes a sound");
}
}
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("The dog barks");
}
}
主要区别:
- C#使用冒号(
:
)表示继承,Java使用extends
关键字。 - C#需要使用
virtual
和override
关键字来实现方法重写,Java只需要使用@Override
注解。 - C#默认使用属性(Properties)而不是getter和setter方法。
构造函数和继承
在C#中,派生类的构造函数可以显式调用基类的构造函数:
public class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
}
}
public class Dog : Animal
{
public string Breed { get; set; }
public Dog(string name, string breed) : base(name)
{
Breed = breed;
}
}
Java中的等效代码:
public class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
}
public class Dog extends Animal {
private String breed;
public Dog(String name, String breed) {
super(name);
this.breed = breed;
}
}
密封类和方法
C#使用sealed
关键字来防止类被继承或方法被重写:
public sealed class FinalClass
{
// 这个类不能被继承
}
public class BaseClass
{
public virtual void VirtualMethod() { }
}
public class DerivedClass : BaseClass
{
public sealed override void VirtualMethod() { }// 这个方法不能在子类中被重写
}
Java使用final
关键字实现类似功能:
public final class FinalClass {
// 这个类不能被继承
}
public class BaseClass {
public void virtualMethod() { }
}
public class DerivedClass extends BaseClass {
@Override
public final void virtualMethod() { }
// 这个方法不能在子类中被重写
}
抽象类和方法
C#和Java都支持抽象类和方法:
C#:
public abstract class Shape
{
public abstract doubleCalculateArea();
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
}
Java:
public abstract class Shape {
public abstract double calculateArea();
}
public class Circle extends Shape {
private double radius;
public void setRadius(double radius) { this.radius = radius; }
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
接口
C#和Java都支持接口,但C# 8.0+允许接口定义默认实现:
C#:
public interface IDrawable
{
void Draw();
void Erase() => Console.WriteLine("Default erase behavior");
}
public class Square : IDrawable
{
public void Draw()
{
Console.WriteLine("Drawing a square");
}// Erase方法使用默认实现
}
Java 8+也支持接口默认方法:
public interface Drawable {
void draw();
default void erase() {
System.out.println("Default erase behavior");
}
}
public class Square implements Drawable {
@Override
public void draw() {
System.out.println("Drawing a square");
}
// erase方法使用默认实现
}
多重继承
C#和Java都不支持类的多重继承,但都允许一个类实现多个接口:
public interface IDrawable
{
void Draw();
}
public interface IResizable
{
void Resize(int width, int height);
}
public class Rectangle : IDrawable, IResizable
{
public void Draw()
{
Console.WriteLine("Drawing a rectangle");
}
public void Resize(int width, int height)
{
Console.WriteLine($"Resizing to {width}x{height}");
}
}
泛型约束
C#支持泛型约束,可以限制泛型参数的类型:
public class GenericRepository<T> where T : class, new()
{
public T CreateNew()
{
return new T();
}
}
Java也支持泛型约束,但语法略有不同:
public class GenericRepository<T extends Object & Serializable> {
public T createNew() throws InstantiationException, IllegalAccessException {
return T.class.newInstance();
}
}
协变和逆变
C#支持泛型接口和委托的协变和逆变:
public interface IEnumerable<out T> { /* ... */ }
public interface IComparer<in T> { /* ... */ }
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // 协变
IComparer<object> objectComparer = /* ... */;
IComparer<string> stringComparer = objectComparer; // 逆变
Java也支持泛型的协变和逆变,但语法不同:
List<String> strings = new ArrayList<>();
List<? extends Object> objects = strings; // 协变
Comparator<Object> objectComparator = /* ... */;
Comparator<? super String> stringComparator = objectComparator; // 逆变
隐藏基类成员
C#使用new
关键字来隐藏基类成员:
public class BaseClass
{
public void Method()
{
Console.WriteLine("BaseClass.Method");
}
}
public class DerivedClass : BaseClass
{
public new void Method()
{
Console.WriteLine("DerivedClass.Method");
}
}
Java没有直接等效的语法,但可以通过重新定义方法来实现类似效果。
基类访问
C#使用base
关键字来访问基类成员,类似于Java中的super
:
public class DerivedClass : BaseClass
{
public override void Method()
{
base.Method(); // 调用基类方法
Console.WriteLine("Additional behavior in derived class");
}
}
构造函数链
C#允许在一个类中定义多个构造函数,并使用this
关键字链接它们:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public Person(string name) : this(name, 0)
{
}
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
显式接口实现
C#允许显式实现接口方法,这在实现多个具有相同方法签名的接口时特别有用:
public interfaceIA
{
void Method();
}
public interface IB
{
void Method();
}
public class MyClass : IA, IB
{
void IA.Method()
{
Console.WriteLine("IA.Method");
}
void IB.Method()
{
Console.WriteLine("IB.Method");
}
public void Method()
{
Console.WriteLine("MyClass.Method");
}
}
Java不支持这种显式接口实现。
抽象属性
C#允许在抽象类中定义抽象属性:
public abstract class Animal
{
public abstract string Sound { get; }
}
public class Dog : Animal
{
public override string Sound => "Woof";
}
派生类中的新成员
C#允许在派生类中添加新的成员,而不需要特殊语法:
public class Animal
{
public virtual void MakeSound() { }
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Woof");
}
public void Fetch() //新方法
{
Console.WriteLine("Dog is fetching");
}
}
泛型继承
C#支持泛型类的继承,并允许在派生类中指定或保持开放泛型类型参数:
public class GenericBase<T>
{
public T Data { get; set; }
}
public class StringDerived : GenericBase<string>
{
public void PrintUpperCase()
{
Console.WriteLine(Data.ToUpper());
}
}
public class GenericDerived<T> : GenericBase<T>
{
public void PrintType()
{
Console.WriteLine(typeof(T).Name);
}
}
接口的多重继承虽然C#不支持类的多重继承,但接口可以继承多个接口:
public interfaceIA
{
void MethodA();
}
public interface IB
{
void MethodB();
}
public interface IC : IA, IB
{
void MethodC();
}
public class MyClass : IC
{
public void MethodA() { }
public void MethodB() { }
public void MethodC() { }
}
继承链中的构造函数调用顺序
在C#中,当创建一个派生类的实例时,构造函数的调用顺序是从最基础的类开始,一直到最派生的类:
public class A
{
public A() { Console.WriteLine("A"); }
}
public class B : A
{
public B() { Console.WriteLine("B"); }
}
public class C : B
{
public C() { Console.WriteLine("C"); }
}
// 使用
var c = new C(); // 输出: A B C
虚方法表(VMT)和动态分发
C#使用虚方法表来实现多态。当你调用一个虚方法时,实际调用的方法是在运行时根据对象的实际类型决定的:
Animal animal = new Dog();
animal.MakeSound(); // 调用Dog的MakeSound方法
这种机制称为动态分发,它是多态的核心实现方式。
接口默认实现中的钻石问题解决
当一个类实现多个具有相同默认方法的接口时,C#要求显式实现该方法:
public interfaceIA
{
void Method() => Console.WriteLine("IA.Method");
}
public interface IB
{
void Method() => Console.WriteLine("IB.Method");
}
public class MyClass : IA, IB
{
public void Method() // 必须实现,否则编译错误
{
((IA)this).Method(); // 调用IA的默认实现
// 或
((IB)this).Method(); // 调用IB的默认实现
}
}
最佳实践
-
优先使用组合而不是继承:继承创建了强耦合,而组合更灵活。
-
遵循里氏替换原则(LSP):子类应该可以替换其基类,而不改变程序的正确性。
-
谨慎使用密封类和方法:虽然它们可以提高性能和安全性,但也限制了扩展性。
-
使用接口进行抽象:接口提供了更好的解耦和灵活性。
-
避免深层继承层次:深层继承可能导致复杂性和维护困难。
-
合理使用抽象类:当你需要在基类中提供一些实现时,使用抽象类而不是接口。
-
正确使用virtual和override关键字:这些关键字明确表达了你的意图,使代码更易理解和维护。
抽象类vs接口
虽然抽象类和接口都可以用来定义抽象类型,但它们有一些关键区别:
- 抽象类可以包含实现,接口(在C# 8.0之前)只能包含方法签名。
- 一个类只能继承一个抽象类,但可以实现多个接口。
- 抽象类可以有构造函数,接口不能。
- 抽象类可以包含字段,接口不能。
选择使用抽象类还是接口取决于你的设计需求:
public abstract class Animal
{
protected string name;
public Animal(string name)
{
this.name = name;
}
public abstract void MakeSound();
}
public interface IMovable
{
void Move();
}
public class Dog : Animal, IMovable
{
public Dog(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine("Woof!");
}
public void Move()
{
Console.WriteLine("Dog is running");
}
}
多接口继承与默认实现
C# 8.0引入的接口默认实现允许我们在不破坏现有代码的情况下向接口添加新方法:
public interface ILogger
{
void Log(string message);void LogError(string message) => Log($"ERROR: {message}");
}
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
// 可以选择重写或使用默认的LogError实现
}
泛型约束中的继承
C#允许在泛型约束中使用继承关系:
public class Animal { }
public class Dog : Animal { }
public class Kennel<T> where T : Animal
{
public void AddAnimal(T animal) { }
}
var dogKennel = new Kennel<Dog>(); // 有效
// var intKennel = new Kennel<int>(); // 编译错误
隐藏继承成员
有时你可能想在派生类中隐藏基类的成员,而不是覆盖它。C#使用new
关键字来实现这一点:
public class Base
{
public virtual void Method()
{
Console.WriteLine("Base.Method");
}
}
public class Derived : Base
{
public new void Method()
{
Console.WriteLine("Derived.Method");
}
}
Base b = new Derived();
b.Method(); // 输出: Base.Method
Derived d = new Derived();
d.Method(); // 输出: Derived.Method
协变和逆变在委托中的应用
C#支持在委托中使用协变和逆变:
delegate T Factory<out T>();
delegate void Action<in T>(T obj);
class Animal { }
class Dog : Animal { }
class Program
{
static Dog CreateDog() => new Dog();
static void HandleAnimal(Animal a) { }
static void Main()
{
Factory<Dog> dogFactory = CreateDog;
Factory<Animal> animalFactory = dogFactory; // 协变
Action<Animal> animalHandler = HandleAnimal;
Action<Dog> dogHandler = animalHandler; // 逆变
}
}
虚拟属性和索引器
C#允许将属性和索引器声明为虚拟的,这样它们可以在派生类中被重写:
public class Base
{
public virtual int Property { get; set; }
public virtual int this[int index]
{
get =>0;
set { }
}
}
public class Derived : Base
{
private int[] data = new int[10];
public override int Property
{
get => base.Property;
set => base.Property = value * 2;
}
public override int this[int index]
{
get => data[index];
set => data[index] = value;
}
}
抽象类中的密封方法
虽然看起来有点矛盾,但C#允许在抽象类中定义密封方法。这可以用来提供一个不能被重写的实现:
public abstract class Base
{
public abstract void AbstractMethod();
public virtual void VirtualMethod() { }
public sealed void SealedMethod() { }
}
public class Derived : Base
{
public override void AbstractMethod() { }
public override void VirtualMethod() { } // 可以重写
//无法重写SealedMethod
}
接口中的静态成员
从C# 8.0开始,接口可以包含静态成员,包括字段、方法、属性等:
public interface IMyInterface
{
static int MyProperty { get; set; }
static void MyMethod() => Console.WriteLine("Hello from interface!");
}
继承链中的构造函数和字段初始化顺序
了解C#中构造函数和字段初始化的顺序非常重要:
- 派生类的字段初始化器
- 派生类的构造函数
- 基类的字段初始化器
- 基类的构造函数
public class Base
{
public int BaseField = 1;
public Base()
{
Console.WriteLine($"Base constructor, BaseField = {BaseField}");
}
}
public class Derived : Base
{
public int DerivedField = BaseField + 1;
public Derived()
{
Console.WriteLine($"Derived constructor, DerivedField = {DerivedField}");
}
}
// 输出:
// Base constructor, BaseField = 1
// Derived constructor, DerivedField = 2
使用接口作为类型参数
接口可以用作方法的类型参数,这提供了更大的灵活性:
public void ProcessItems<T>(IEnumerable<T> items) where T : IComparable<T>
{
foreach (var item in items.OrderBy(i => i))
{
Console.WriteLine(item);
}
}
小结
C#提供了丰富而灵活的继承和多态机制,允许开发者创建复杂的类层次结构和接口设计。相比Java,C#在某些方面提供了更细粒度的控制和更多的语法糖,如显式接口实现、抽象属性等。
作为一个从Java转向C#的开发者,你会发现这些特性可以帮助你创建更加清晰、灵活和可维护的代码。例如,显式接口实现可以解决方法名冲突问题,抽象属性可以更优雅地定义必须由派生类实现的属性。
然而,重要的是要记住,这些强大的特性也带来了复杂性。在使用继承和多态时,始终要考虑代码的可维护性和可读性。遵循SOLID原则,合理使用组合和继承,避免创建过于复杂的继承层次结构。
通过深入理解和合理使用这些特性,你可以充分利用C#的强大功能,创建出更加灵活、可扩展和易于维护的代码。在实际项目中,根据具体需求和设计目标来选择合适的特性和模式,将帮助你成为一个更出色的C#开发者。
当然,让我们继续深入探讨C#中继承和多态的一些高级概念和最佳实践:
抽象工厂模式
抽象工厂模式是一种创建型设计模式,它使用继承和多态来提供一种创建相关对象家族的方式,而无需指定它们的具体类。这在C#中可以优雅地实现:
public interface IButton { void Paint(); }
public interface ICheckbox { void Paint(); }
public interface IGUIFactory
{
IButton CreateButton();
ICheckbox CreateCheckbox();
}
public class WinButton : IButton
{
public void Paint() => Console.WriteLine("Windows Button");
}
public class MacButton : IButton
{
public void Paint() => Console.WriteLine("Mac Button");
}
public class WinCheckbox : ICheckbox
{
public void Paint() => Console.WriteLine("Windows Checkbox");
}
public class MacCheckbox : ICheckbox
{
public void Paint() => Console.WriteLine("Mac Checkbox");
}
public class WinFactory : IGUIFactory
{
public IButton CreateButton() => new WinButton();
public ICheckbox CreateCheckbox() => new WinCheckbox();
}
public class MacFactory : IGUIFactory
{
public IButton CreateButton() => new MacButton();
public ICheckbox CreateCheckbox() => new MacCheckbox();
}
// 使用
IGUIFactory factory = new WinFactory();
var button = factory.CreateButton();
button.Paint(); //输出: Windows Button
模板方法模式
模板方法模式定义了一个算法的骨架,允许子类重新定义算法的某些步骤,而不改变算法的结构:
public abstract class DataProcessor
{
public void ProcessData()
{
OpenFile();
ExtractData();
ParseData();
AnalyzeData();
SendReport();
CloseFile();
}
protected abstract void ExtractData();
protected abstract void ParseData();
protected abstract void AnalyzeData();
protected virtual void OpenFile()
{
Console.WriteLine("Opening file...");
}
protected virtual void SendReport()
{
Console.WriteLine("Sending report...");
}
protected virtual void CloseFile()
{
Console.WriteLine("Closing file...");
}
}
public class PDFProcessor : DataProcessor
{
protected override void ExtractData()
{
Console.WriteLine("Extracting data from PDF...");
}
protected override void ParseData()
{
Console.WriteLine("Parsing PDF data...");
}
protected override void AnalyzeData()
{
Console.WriteLine("Analyzing PDF data...");
}
}
策略模式
策略模式定义了一系列算法,并使它们可以互相替换。这个模式利用了多态性:
public interface ISortStrategy
{
void Sort(List<int> data);
}
public class BubbleSort : ISortStrategy
{
public void Sort(List<int> data)
{
Console.WriteLine("Performing bubble sort");
// 实现冒泡排序
}
}
public class QuickSort : ISortStrategy
{
public void Sort(List<int> data)
{
Console.WriteLine("Performing quick sort");
// 实现快速排序
}
}
public class Sorter
{
private ISortStrategy _strategy;
public Sorter(ISortStrategy strategy)
{
_strategy = strategy;
}
public void SetStrategy(ISortStrategy strategy)
{
_strategy = strategy;
}
public void SortData(List<int> data)
{
_strategy.Sort(data);
}
}
// 使用
var sorter = new Sorter(new BubbleSort());
sorter.SortData(new List<int> { 3, 1, 4, 1, 5, 9});
sorter.SetStrategy(new QuickSort());
sorter.SortData(new List<int> { 3, 1, 4, 1, 5, 9 });
装饰器模式
装饰器模式允许你动态地给对象添加新的行为,而不改变其结构:
public interface ICoffee
{
string GetDescription();
double GetCost();
}
public class SimpleCoffee : ICoffee
{
public string GetDescription() => "Simple Coffee";
public double GetCost() => 1;
}
public abstract class CoffeeDecorator : ICoffee
{
protected ICoffee _coffee;
public CoffeeDecorator(ICoffee coffee)
{
_coffee = coffee;
}
public virtual string GetDescription() => _coffee.GetDescription();
public virtual double GetCost() => _coffee.GetCost();
}
public class MilkDecorator : CoffeeDecorator
{
public MilkDecorator(ICoffee coffee) : base(coffee) { }
public override string GetDescription() => $"{base.GetDescription()}, Milk";
public override double GetCost() => base.GetCost() + 0.5;
}
public class SugarDecorator : CoffeeDecorator
{
public SugarDecorator(ICoffee coffee) : base(coffee) { }
public override string GetDescription() => $"{base.GetDescription()}, Sugar";
public override double GetCost() => base.GetCost() + 0.2;
}
// 使用
ICoffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
Console.WriteLine($"Description: {coffee.GetDescription()}");
Console.WriteLine($"Cost: ${coffee.GetCost()}");
使用协变和逆变优化接口设计
协变和逆变可以使得泛型接口更加灵活:
// 协变接口
public interface IProducer<out T>
{
T Produce();
}
// 逆变接口
public interface IConsumer<in T>
{
void Consume(T item);
}
public class Animal { }
public class Dog : Animal { }
public class DogProducer : IProducer<Dog>
{
public Dog Produce() => new Dog();
}
public class AnimalConsumer : IConsumer<Animal>
{
public void Consume(Animal animal) { }
}
// 使用
IProducer<Animal> animalProducer = new DogProducer(); // 协变
IConsumer<Dog> dogConsumer = new AnimalConsumer(); // 逆变
使用接口隔离原则(ISP)
接口隔离原则建议将大接口分割成更小、更具体的接口:
// 不好的设计
public interface IWorker
{
void Work();
void Eat();void Sleep();
}
// 更好的设计
public interface IWorkable
{
void Work();
}
public interface IEatable
{
void Eat();
}
public interface ISleepable
{
void Sleep();
}
public class Human : IWorkable, IEatable, ISleepable
{
public void Work() { }
public void Eat() { }
public void Sleep() { }
}
public class Robot : IWorkable
{
public void Work() { }
}
使用组合优于继承虽然继承是一个强大的工具,但它也可能导致紧耦合。
在许多情况下,组合可能是一个更好的选择:
// 使用继承
public class Bird
{
public virtual void Fly()
{
Console.WriteLine("Flying...");
}
}
public class Penguin : Bird
{
public override void Fly()
{
throw new NotSupportedException("Penguins can't fly");
}
}
// 使用组合
public interface IFlyable
{
void Fly();
}
public class FlyingBehavior : IFlyable
{
public void Fly()
{
Console.WriteLine("Flying...");
}
}
public class Bird
{
private IFlyable _flyingBehavior;
public Bird(IFlyable flyingBehavior)
{
_flyingBehavior = flyingBehavior;
}
public void Fly()
{
_flyingBehavior.Fly();
}
}
public class Penguin : Bird
{
public Penguin() : base(new NullFlyingBehavior()) { }
}
public class NullFlyingBehavior : IFlyable
{
public void Fly() { } // Do nothing
}
小结
这些高级概念和模式展示了如何在实际应用中利用C#的继承和多态特性来创建灵活、可扩展的代码。设计模式如抽象工厂、策略、装饰器等,都是继承和多态的具体应用,可以帮助你解决常见的设计问题。
同时,我们也看到了一些重要的设计原则,如接口隔离原则和组合优于继承。这些原则可以帮助你创建更加模块化、易于维护的代码。
在实际开发中,选择使用继承还是组合,或者选择哪种设计模式,都需要根据具体的问题和上下文来决定。重要的是要理解这些概念背后的原理,并能够灵活地应用它们。
作为一个从Java转向C#的开发者,你会发现这些概念在两种语言中都是适用的,但C#提供了一些额外的特性(如显式接口实现、扩展方法等)可以让这些模式的实现更加优雅。
继续深入学习和实践这些概念,你将能够设计出更加健壮、灵活的软件系统,成为一个更优秀的C#开发者。
1.8 接口和抽象类
接口和抽象类都是C#中用于定义抽象类型的重要工具,但它们有不同的用途和特性。让我们详细探讨这两个概念,并比较它们与Java中对应概念的异同。
接口(Interfaces)
接口在C#中定义了一组方法、属性、事件或索引器的签名,但不提供实现。它们用于定义一个类应该具有的能力,而不关心这些能力如何实现。
C#中接口的基本语法:
public interface IAnimal
{
string Name { get; set; }
void MakeSound();
}
public class Dog : IAnimal
{
public string Name { get; set; }
public void MakeSound()
{
Console.WriteLine("Woof!");
}
}
C# 8.0中的接口新特性:
- 默认实现:
public interface IAnimal
{
string Name { get; set; }
void MakeSound();
void Eat() => Console.WriteLine($"{Name} is eating.");
}
- 静态成员:
public interface IAnimal
{
static int Count { get; set; }
static void IncrementCount() => Count++;
}
- 私有成员:
public interface IAnimal
{
private static int count;
public static int Count => count;static void IncrementCount() => count++;
}
与Java的比较:
Java8引入了接口默认方法,类似于C# 8.0的默认实现。但C#的接口更加灵活,允许静态成员和私有成员。
抽象类(Abstract Classes)
抽象类是一种不能被实例化的类,用于定义其他类的共同特征。它可以包含抽象方法(没有实现的方法)和具体方法(有实现的方法)。
C#中抽象类的基本语法:
public abstract class Animal
{
public string Name { get; set; }
public abstract void MakeSound();
public virtual void Eat()
{
Console.WriteLine($"{Name} is eating.");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Woof!");
}
}
与Java的比较:
C#和Java的抽象类概念非常相似。两种语言中,抽象类都可以包含抽象方法和具体方法,都不能被实例化,都可以被其他类继承。
接口 vs 抽象类
让我们比较一下接口和抽象类的主要区别:
-
多重继承:
- C#类可以实现多个接口,但只能继承一个类(包括抽象类)。
- Java的情况相同。
-
成员类型:
- C# 8.0之前,接口只能包含方法、属性、事件和索引器的签名。
- C# 8.0及以后,接口可以包含默认实现、静态成员和私有成员。
-抽象类可以包含字段、构造函数、析构函数,以及实现方法。
-
访问修饰符:
- 接口成员默认是公共的,不能有私有成员(C# 8.0之前)。
- 抽象类可以有各种访问修饰符的成员。
-
构造函数:
- 接口不能包含构造函数。
- 抽象类可以包含构造函数。
-
密封成员:
- 接口不能有密封成员。
- 抽象类可以包含密封方法,防止它们被重写。
何时使用接口,何时使用抽象类?
-
使用接口当:
- 你想定义一个能力或行为,而不关心实现细节。
- 你需要多重继承。
- 你想在不相关的类之间定义共同的行为。
-
使用抽象类当:
- 你想定义一个模板,其中包含一些共同的实现。
- 你需要在相关的类之间共享代码。
- 你需要定义非公共的成员。
实际应用示例
让我们看一个更复杂的例子,展示接口和抽象类的结合使用:
public interface IPayable
{
decimalCalculatePay();
}
public abstract class Employee : IPayable
{
public string Name { get; set; }
public int Id { get; set; }
protected Employee(string name, int id)
{
Name = name;
Id = id;
}
public abstract decimal CalculatePay();
public virtual void DisplayInfo()
{
Console.WriteLine($"Employee: {Name}, ID: {Id}");
}
}
public class FullTimeEmployee : Employee
{
public decimal MonthlySalary { get; set; }
public FullTimeEmployee(string name, int id, decimal monthlySalary) : base(name, id)
{
MonthlySalary = monthlySalary;
}
public override decimal CalculatePay()
{
return MonthlySalary;
}
}
public class PartTimeEmployee : Employee
{
public decimal HourlyRate { get; set; }
public int HoursWorked { get; set; }
public PartTimeEmployee(string name, int id, decimal hourlyRate) : base(name, id)
{
HourlyRate = hourlyRate;
}
public override decimal CalculatePay()
{
return HourlyRate * HoursWorked;
}
public override void DisplayInfo()
{
base.DisplayInfo();
Console.WriteLine($"Hourly Rate: {HourlyRate}");
}
}
public class Contractor : IPayable
{
public string Name { get; set; }
public decimal ContractAmount { get; set; }
public Contractor(string name, decimal contractAmount)
{
Name = name;
ContractAmount = contractAmount;
}
public decimal CalculatePay()
{
return ContractAmount;
}
}
// 使用示例
public class PayrollSystem
{
public void ProcessPayroll(List<IPayable> payables)
{
foreach (var payable in payables)
{
Console.WriteLine($"Paying {payable.CalculatePay():C}");
if (payable is Employee employee)
{
employee.DisplayInfo();
}}
}
}
// 主程序
class Program
{
static void Main(string[] args)
{
var payables = new List<IPayable>
{
new FullTimeEmployee("Alice", 1, 5000m),
new PartTimeEmployee("Bob", 2, 20m) { HoursWorked = 80 },
new Contractor("Charlie", 3000m)
};
var payrollSystem = new PayrollSystem();
payrollSystem.ProcessPayroll(payables);
}
}
在这个例子中:
IPayable
接口定义了一个通用的支付计算方法。Employee
抽象类实现了IPayable
接口,并提供了一些共同的功能。FullTimeEmployee
和PartTimeEmployee
继承自Employee
,实现了特定的薪资计算逻辑。Contractor
直接实现了IPayable
接口,因为它与雇员有不同的特征。PayrollSystem
利用多态性处理不同类型的可支付对象。
设计技巧
- 接口隔离原则:创建小而专注的接口,而不是一个大而全的接口。
public interface IWorkable
{
void Work();
}
public interface IEatable
{
void Eat();
}
public class Human : IWorkable, IEatable
{
public void Work() { /* ... */ }
public void Eat() { /* ... */ }
}
public class Robot : IWorkable
{
public void Work() { /* ... */ }
}
- 使用抽象类作为模板:
public abstract class ReportGenerator
{
public void GenerateReport()
{
CollectData();
FormatData();
OutputReport();
}
protected abstract void CollectData();
protected abstract void FormatData();
protected virtual void OutputReport()
{
Console.WriteLine("Outputting report...");
}
}
public class PDFReportGenerator : ReportGenerator
{
protected override void CollectData() { /* ... */ }
protected override void FormatData() { /* ... */ }
//使用基类的OutputReport
}
- 组合接口创建更复杂的契约:
public interface IReadable
{
string Read();
}
public interface IWritable
{
void Write(string content);
}
public interface IFile : IReadable, IWritable
{
string Name { get; set; }
}
public class TextFile : IFile
{
public string Name { get; set; }
public string Read() { /* ... */ }
public void Write(string content) { /* ... */ }
}
最佳实践
-
优先使用接口:接口提供了更好的灵活性和可测试性。
-
抽象类用于定义模板:当你有一组相关的类需要共享一些实现时,使用抽象类。
-
利用C# 8.0的新特性:默认实现可以帮助你在不破坏现有代码的情况下扩展接口。
-
考虑可测试性:接口更容易模拟(mock),有利于单元测试。
-
避免过度抽象:只有当你确实需要抽象时才创建接口或抽象类。
6.遵循SOLID原则:特别是单一责任原则和接口隔离原则。
小结
接口和抽象类是C#中两个强大的抽象工具,它们各有优势和适用场景。接口提供了更大的灵活性和多重继承的能力,而抽象类则允许你定义共同的实现和状态。在设计中合理使用这两种工具,可以创建出更加灵活、可维护和可扩展的代码结构。
作为一个从Java转向C#的开发者,你会发现这两个概念在两种语言中有很多相似之处。但C# 8.0引入的接口新特性,如默认实现和私有成员,为接口提供了更多的功能,使得接口和抽象类之间的界限变得更加模糊。这为代码设计提供了更多的选择和灵活性。
在实际开发中,根据具体需求选择合适的抽象工具,并遵循良好的设计原则,将帮助你创建出高质量的C#代码。随着你对这些概念的深入理解和实践,你将能够更好地利用C#语言的特性,设计出优雅而强大的软件架构。
很好,让我们继续深入探讨接口和抽象类的高级应用,以及它们在现代C#开发中的最佳实践。
高级应用场景
1. 依赖注入与接口
依赖注入是现代软件开发中的一个重要概念,特别是在使用Asp.Net Core等框架时。接口在这里扮演着关键角色。
public interface IUserRepository
{
Task<User> GetUserByIdAsync(int id);
Task<IEnumerable<User>> GetAllUsersAsync();
Task AddUserAsync(User user);
}
public class UserRepository : IUserRepository
{
private readonly ApplicationDbContext _context;
public UserRepository(ApplicationDbContext context)
{
_context = context;
}
public async Task<User> GetUserByIdAsync(int id)
{
return await _context.Users.FindAsync(id);
}
public async Task<IEnumerable<User>> GetAllUsersAsync()
{
return await _context.Users.ToListAsync();
}
public async Task AddUserAsync(User user)
{
await _context.Users.AddAsync(user);
await _context.SaveChangesAsync();
}
}
// 在Startup.cs中注册服务
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IUserRepository, UserRepository>();
}
// 在控制器中使用
public class UserController :ControllerBase
{
private readonly IUserRepository _userRepository;
public UserController(IUserRepository userRepository)
{
_userRepository = userRepository;
}
[HttpGet("{id}")]
public async Task<ActionResult<User>> GetUser(int id)
{
var user = await _userRepository.GetUserByIdAsync(id);
if (user == null)
{
return NotFound();
}
return user;
}
}
这种方法允许我们轻松地替换实现,例如,我们可以创建一个模拟版本用于测试:
public class MockUserRepository : IUserRepository
{
private readonly List<User> _users = new();
public async Task<User> GetUserByIdAsync(int id)
{
return await Task.FromResult(_users.FirstOrDefault(u => u.Id == id));
}
// 实现其他方法...
}
// 在测试中使用
[Fact]
public async Task GetUser_ReturnsUser_WhenUserExists()
{
// Arrange
var mockRepo = new MockUserRepository();
var controller = new UserController(mockRepo);
var testUser = new User { Id = 1, Name = "Test User" };
await mockRepo.AddUserAsync(testUser);
// Act
var result = await controller.GetUser(1);
// Assert
var actionResult = Assert.IsType<ActionResult<User>>(result);
var returnValue = Assert.IsType<User>(actionResult.Value);
Assert.Equal(testUser.Id, returnValue.Id);
Assert.Equal(testUser.Name, returnValue.Name);
}
2. 抽象类作为领域模型的基类
在领域驱动设计(DDD)中,抽象类常常用作实体或值对象的基类。
public abstract class Entity
{
public int Id { get; protected set; }
public override bool Equals(object obj)
{
var other = obj as Entity;
if (ReferenceEquals(other, null))
return false;
if (ReferenceEquals(this, other))
return true;
if (GetType() != other.GetType())
return false;
if (Id ==0 || other.Id == 0)
return false;
return Id == other.Id;
}
public static bool operator ==(Entity a, Entity b)
{
if (ReferenceEquals(a, null) && ReferenceEquals(b, null))
return true;
if (ReferenceEquals(a, null) || ReferenceEquals(b, null))
return false;
return a.Equals(b);
}
public static bool operator !=(Entity a, Entity b)
{
return !(a == b);
}
public override int GetHashCode()
{
return (GetType().ToString() + Id).GetHashCode();
}
}
public class User : Entity
{
public string Name { get; set; }
public string Email { get; set; }
}
public class Product : Entity
{
public string Name { get; set; }
public decimal Price { get; set; }
}
这个抽象基类提供了通用的相等性比较逻辑,所有继承自它的实体都会自动获得这些功能。
3. 接口默认实现的高级应用
C# 8.0引入的接口默认实现可以用于创建可选功能或提供默认行为。
public interface ILogger
{
void Log(string message);
void LogError(string message) => Log($"ERROR: {message}");
void LogWarning(string message) => Log($"WARNING: {message}");
void LogInfo(string message) => Log($"INFO: {message}");
}
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"[{DateTime.Now}] {message}");
}
// 可以选择性地重写默认实现
public void LogError(string message)
{
Console.ForegroundColor = ConsoleColor.Red;
Log($"ERROR: {message}");
Console.ResetColor();
}
}
public class FileLogger : ILogger
{
private readonly string _path;
public FileLogger(string path)
{
_path = path;
}
public void Log(string message)
{
File.AppendAllText(_path, $"[{DateTime.Now}] {message}\n");
}
// 使用默认实现的其他方法
}
这种方法允许我们在接口中定义通用行为,同时保留了实现类专它的灵活性。
最佳实践和设计模式
1. 策略模式
策略模式是一个典型的使用接口的设计模式:
public interface IPaymentStrategy
{
void Pay(decimal amount);
}
public class CreditCardPayment : IPaymentStrategy
{
public void Pay(decimal amount)
{
Console.WriteLine($"Paying {amount} using Credit Card");
}
}
public class PayPalPayment : IPaymentStrategy
{
public void Pay(decimal amount)
{
Console.WriteLine($"Paying {amount} using PayPal");
}
}
public class ShoppingCart
{
private readonly IPaymentStrategy _paymentStrategy;
public ShoppingCart(IPaymentStrategy paymentStrategy)
{
_paymentStrategy = paymentStrategy;
}
public void Checkout(decimal amount)
{
_paymentStrategy.Pay(amount);
}
}
// 使用
var cart = new ShoppingCart(new CreditCardPayment());
cart.Checkout(100.50m);
cart = new ShoppingCart(new PayPalPayment());
cart.Checkout(200.75m);
2. 模板方法模式
模板方法模式是抽象类的一个经典应用:
public abstract class PizzaMaker
{
public void MakePizza()
{
PrepareDough();
AddSauce();
AddToppings();Bake();
Cut();
}
protected abstract void AddToppings();
protected virtual void PrepareDough()
{
Console.WriteLine("Preparing pizza dough");
}
protected virtual void AddSauce()
{
Console.WriteLine("Adding tomato sauce");
}
protected virtual void Bake()
{
Console.WriteLine("Baking for 15 minutes at 200°C");
}
protected virtual void Cut()
{
Console.WriteLine("Cutting the pizza into 8 slices");
}
}
public class MargheritaPizza : PizzaMaker
{
protected override void AddToppings()
{
Console.WriteLine("Adding mozzarella and basil");
}
}
public class PepperoniPizza : PizzaMaker
{
protected override void AddToppings()
{
Console.WriteLine("Adding pepperoni and extra cheese");
}
protected override void Bake()
{
Console.WriteLine("Baking for 20 minutes at 180°C");
}
}
// 使用
var margherita = new MargheritaPizza();
margherita.MakePizza();
var pepperoni = new PepperoniPizza();
pepperoni.MakePizza();
高级技巧
- 使用泛型约束与接口:
public interface IEntity
{
int Id { get; set; }
}
public interface IRepository<T> where T : class, IEntity
{
T GetById(int id);
void Add(T entity);
void Update(T entity);
void Delete(int id);
}
public class GenericRepository<T> : IRepository<T> where T : class, IEntity
{
private readonly DbContext _context;
private readonly DbSet<T> _dbSet;
public GenericRepository(DbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public T GetById(int id)
{
return _dbSet.Find(id);
}
public void Add(T entity)
{
_dbSet.Add(entity);
_context.SaveChanges();
}
public void Update(T entity)
{
_dbSet.Attach(entity);
_context.Entry(entity).State = EntityState.Modified;
_context.SaveChanges();
}
public void Delete(int id)
{
var entity = GetById(id);
_dbSet.Remove(entity);
_context.SaveChanges();
}
}
- 使用接口进行多重继承:
public interface IFlying
{
void Fly();
}
public interface ISwimming
{
void Swim();
}
public class Duck : IFlying, ISwimming
{
public void Fly()
{
Console.WriteLine("Duck is flying");
}
public void Swim()
{
Console.WriteLine("Duck is swimming");
}
}
public class FlyingFish : IFlying, ISwimming
{
public void Fly()
{
Console.WriteLine("Flying fish is gliding through the air");
}
public void Swim()
{
Console.WriteLine("Flying fish is swimming");
}
}
- 使用显式接口实现来解决方法名冲突:
public interface IWork
{
void Start();
}
public interface IEngine
{
void Start();
}
public class Car : IWork, IEngine
{
void IWork.Start()
{
Console.WriteLine("Starting work");
}
void IEngine.Start()
{
Console.WriteLine("Starting engine");
}
public void Start()
{
Console.WriteLine("Car is starting");
}
}
// 使用
Car car = new Car();
car.Start(); // 输出: Car is starting
IWork work = car;
work.Start(); // 输出: Starting work
IEngine engine = car;
engine.Start(); // 输出: Starting engine
结论
接口和抽象类是C#中强大的抽象化工具,它们在软件设计中扮演着关键角色。通过正确使用这些工具,我们可以创建更加灵活、可维护和可扩展的代码结构。
- 接口适用于定义对象的能力或行为,特别是在需要多重继承或定义跨类型契约时。
- 抽象类适用于定义一组相关类的共同特征和行为,特别是在需要共享实现代码时。
在实际开发中,你可能会发现同时使用这两种工具的情况。例如,你可能会创建一个抽象基类来实现一个或多个接口,然后让具体类继承这个抽象基类。
记住,好的设计不仅仅是使用正确的工具,还包括遵循SOLID原则、设计模式和最佳实践。随着你在C#开发中积累更多经验,你将能够更加自如地在不同场景中选择和应用这些抽象化工具。
最后,随着C#语言的不断发展(特别是C# 8.0引入的接口新特性),保持对语言新特性的了解和学习也很重要。这些新特性可能会为你提供更多的设计选择,帮助你写出更好的代码。
1.9 委托和事件
委托是C#中的一种类型,它表示对具有特定参数列表和返回类型的方法的引用。事件则是基于委托的一种机制,用于对象之间的通信。让我们深入了解这两个概念。
1.9.1 委托(Delegates)
委托允许将方法作为参数传递,这为实现回调和事件处理提供了基础。
基本语法
// 定义委托
public delegate void SimpleDelegate(string message);
// 使用委托
public class DelegateDemo
{
public void DisplayMessage(string message)
{
Console.WriteLine($"Message: {message}");
}
public void Run()
{
SimpleDelegate del = DisplayMessage;
del("Hello, Delegates!");
}
}
// 使用
var demo = new DelegateDemo();
demo.Run();// 输出: Message: Hello, Delegates!
多播委托
委托可以指向多个方法,这被称为多播委托。
public class MulticastDelegateDemo
{
public static void Method1(string message)
{
Console.WriteLine($"Method1: {message}");
}
public static void Method2(string message)
{
Console.WriteLine($"Method2: {message}");
}
public void Run()
{
SimpleDelegate del = Method1;
del += Method2; // 添加另一个方法
del("Hello, Multicast!");
}
}
// 使用
var demo = new MulticastDelegateDemo();
demo.Run();
// 输出:
// Method1: Hello, Multicast!
// Method2: Hello, Multicast!
匿名方法和Lambda 表达式
C# 2.0 引入了匿名方法,C# 3.0 进一步引入了 Lambda 表达式,它们都可以用来创建内联委托。
public class AnonymousAndLambdaDemo
{
public void Run()
{
// 匿名方法
SimpleDelegate anonymousDelegate = delegate(string message)
{
Console.WriteLine($"Anonymous: {message}");
};
// Lambda 表达式
SimpleDelegate lambdaDelegate = (message) => Console.WriteLine($"Lambda: {message}");
anonymousDelegate("Hello from anonymous method!");
lambdaDelegate("Hello from lambda expression!");
}
}
1.9.2 事件(Events)
事件提供了一种方式,使得一个类可以通知其他类发生了某些事情。事件使用委托来实现。
基本语法
public class Publisher
{
// 定义一个委托类型
public delegate void EventHandler(string message);
// 声明一个事件
public event EventHandler SomethingHappened;
public void DoSomething()
{
// 触发事件
SomethingHappened?.Invoke("Something just happened!");
}
}
public class Subscriber
{
public void HandleEvent(string message)
{
Console.WriteLine($"Event handled: {message}");
}
}
// 使用
var publisher = new Publisher();
var subscriber = new Subscriber();
publisher.SomethingHappened += subscriber.HandleEvent;
publisher.DoSomething();// 输出: Event handled: Something just happened!
标准事件模式
.NET Framework 定义了一个标准的事件模式,使用EventHandler
和 EventArgs
。
public class CustomEventArgs : EventArgs
{
public string Message { get; set; }
}
public class StandardEventPublisher
{
public event EventHandler<CustomEventArgs> CustomEvent;
protected virtual void OnCustomEvent(CustomEventArgs e)
{
CustomEvent?.Invoke(this, e);
}
public void TriggerEvent()
{
OnCustomEvent(new CustomEventArgs { Message = "Custom event triggered" });
}
}
public class StandardEventSubscriber
{
public void HandleCustomEvent(object sender, CustomEventArgs e)
{
Console.WriteLine($"Event received: {e.Message}");
}
}
// 使用
var publisher = new StandardEventPublisher();
var subscriber = new StandardEventSubscriber();
publisher.CustomEvent += subscriber.HandleCustomEvent;
publisher.TriggerEvent(); // 输出: Event received: Custom event triggered
1.9.3 委托和事件的高级应用
1. 异步编程
委托在异步编程中扮演重要角色,尽管现在更常用的是 Task-based 异步模式。
public class AsyncDelegateDemo
{
public delegate int AsyncCalculation(int x, int y);
public static int Add(int x, int y)
{
Thread.Sleep(1000); // 模拟耗时操作
return x + y;
}
public async Task RunAsync()
{
AsyncCalculation del = Add;
var result = await Task.Run(() => del(5, 3));
Console.WriteLine($"Async result: {result}");
}
}
2. LINQ 和函数式编程
委托是LINQ和函数式编程风格的基础。
public class LinqDemo
{
public void Run()
{
var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var evenNumbers = numbers.Where(n => n % 2 == 0).Select(n => n * n).ToList();
Console.WriteLine(string.Join(", ", evenNumbers));
}
}
3. 事件聚合器
在复杂的应用程序中,可以使用事件聚合器模式来集中管理事件。
public class EventAggregator
{
private Dictionary<Type, List<object>> eventHandlers = new Dictionary<Type, List<object>>();
public void Subscribe<TEvent>(Action<TEvent> handler)
{
var eventType = typeof(TEvent);
if (!eventHandlers.ContainsKey(eventType))
{
eventHandlers[eventType] = new List<object>();
}
eventHandlers[eventType].Add(handler);
}
public void Publish<TEvent>(TEvent eventToPublish)
{
var eventType = typeof(TEvent);
if (eventHandlers.ContainsKey(eventType))
{
foreach (var handler in eventHandlers[eventType].Cast<Action<TEvent>>())
{
handler(eventToPublish);
}
}
}
}
// 使用
public class EventAggregatorDemo
{
public class UserLoggedInEvent
{
public string Username { get; set; }
}
public void Run()
{
var aggregator = new EventAggregator();
aggregator.Subscribe<UserLoggedInEvent>(e =>Console.WriteLine($"User logged in: {e.Username}"));
aggregator.Publish(new UserLoggedInEvent { Username = "JohnDoe" });
}
}
1.10.4 最佳实践
- 使用
EventHandler
和EventHandler<TEventArgs>
作为事件委托类型。 - 总是检查事件是否为null 再调用(使用
?.
操作符)。 - 考虑使用
System.Action
和System.Func
代替自定义委托类型。 - 在多线程环境中使用事件时要小心,考虑线程安全问题。
- 避免在循环中频繁触发事件,考虑批处理或节流。
// 好的实践
public event EventHandler<CustomEventArgs> GoodEvent;
protected virtual void OnGoodEvent(CustomEventArgs e)
{
GoodEvent?.Invoke(this, e);
}
// 避免的做法
public delegate void BadEventDelegate(int someParameter);
public event BadEventDelegate BadEvent;
public void TriggerBadEvent()
{
if (BadEvent != null)
{
BadEvent(42); // 不安全,可能在检查和调用之间变为null
}
}
结论
委托和事件是C#中非常强大的特性,它们为实现灵活的、松耦合的代码提供了基础。委托允许方法作为参数传递,这为回调、异步编程和LINQ等功能提供了支持。事件则建立在委托的基础上,提供了一种标准的、类型安全的方式来实现发布-订阅模式。
通过使用委托和事件,你可以创建更加模块化和可扩展的代码。它们在图形用户界面编程、异步编程、事件驱动系统等多个领域都有广泛应用。
然而,使用委托和事件也需要谨慎。过度使用可能导致代码难以追踪和调试。在设计系统时,应该仔细考虑何时使用委托和事件,以及如何最好地组织和管理它们。
随着你在C#开发中积累更多经验,你会发现委托和事件在各种场景中的巧妙应用。掌握这些概念将帮助你编写更加灵活、高效的代码,并更好地理解和利用.NET框架的许多高级特性。
1.10泛型
欢迎来到我们的C#泛型深度探索之旅!对于从Java转向.NET Core的开发者来说,泛型可能是一个熟悉而又陌生的概念。虽然两种语言都支持泛型,但C#的泛型实现在某些方面更加强大和灵活。让我们一起来探讨这些差异,并通过实际案例来加深理解。
1.10.1 泛型基础
首先,让我们回顾一下泛型的基本概念。泛型允许我们编写可以处理多种数据类型的代码,而不需要为每种类型都编写重复的代码。这不仅提高了代码的复用性,也增强了类型安全性。
C#示例:
public class GenericList<T>
{
private List<T> items = new();
public void Add(T item) => items.Add(item);
public T GetItem(int index) => items[index];
}
// 使用示例
var intList = new GenericList<int>();
intList.Add(10);
Console.WriteLine(intList.GetItem(0)); // 输出: 10
var stringList = new GenericList<string>();
stringList.Add("Hello");
Console.WriteLine(stringList.GetItem(0)); // 输出: Hello
Java示例:
public class GenericList<T> {
private List<T> items = new ArrayList<>();
public void add(T item) {
items.add(item);
}
public T getItem(int index) {
return items.get(index);
}
}
// 使用示例
GenericList<Integer> intList = new GenericList<>();
intList.add(10);
System.out.println(intList.getItem(0)); // 输出: 10
GenericList<String> stringList = new GenericList<>();
stringList.add("Hello");
System.out.println(stringList.getItem(0)); // 输出: Hello
看起来很相似,对吧?但是,让我们深入探讨一下C#泛型的一些独特特性。
1.10.2 泛型约束
C#允许我们对泛型类型参数添加约束,这在Java中是没有直接对应的功能。
C#示例:
public class Calculator<T> where T : struct, IComparable<T>
{
public T Max(T a, T b) => a.CompareTo(b) > 0 ? a : b;
}
// 使用示例
var calc = new Calculator<int>();
Console.WriteLine(calc.Max(5, 10)); // 输出: 10
// 下面的代码会导致编译错误,因为string不是值类型
// var stringCalc = new Calculator<string>();
在这个例子中,我们限制了T必须是值类型(struct)并且实现了IComparable接口。
Java的近似实现:
Java没有直接的泛型约束,但可以通过接口来实现类似的功能:
public class Calculator<T extends Comparable<T>> {
public T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
}
// 使用示例
Calculator<Integer> calc = new Calculator<>();
System.out.println(calc.max(5, 10)); // 输出: 10
// 这是允许的,因为Java没有值类型的概念
Calculator<String> stringCalc = new Calculator<>();
System.out.println(stringCalc.max("apple", "banana")); // 输出: banana
注意,Java的泛型约束相对较弱,无法像C#那样限制为值类型。
1.10.3 泛型协变和逆变
C#支持泛型接口和委托的协变和逆变,这为类型系统带来了更大的灵活性。Java从1.5版本开始也支持协变,但范围较小。
C#示例:
// 协变
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // 这是允许的
// 逆变
Action<object> objectAction = obj => Console.WriteLine(obj);
Action<string> stringAction = objectAction; // 这是允许的
// 同时使用协变和逆变
interface IConverter<in TInput, out TOutput>
{
TOutput Convert(TInput input);
}
class StringToIntConverter : IConverter<string, int>
{
public int Convert(string input) => int.Parse(input);
}
IConverter<object, int> converter = new StringToIntConverter();
int result = converter.Convert("42");
Console.WriteLine(result); // 输出: 42
Java示例:
Java的泛型协变较为有限,主要通过通配符实现:
// 协变(只读)
List<String> strings = new ArrayList<>();
List<? extends Object> objects = strings; // 这是允许的
// Java不支持泛型方法的逆变
// 下面的代码在Java中是不允许的:
// Consumer<Object> objectConsumer = obj -> System.out.println(obj);
// Consumer<String> stringConsumer = objectConsumer;
// Java中没有直接对应C#的in和out关键字的概念
interface Converter<T, R> {
R convert(T input);
}
class StringToIntConverter implements Converter<String, Integer> {
@Override
public Integer convert(String input) {
return Integer.parseInt(input);
}
}
// 在Java中,不能像C#那样直接赋值
// Converter<Object, Integer> converter = new StringToIntConverter(); // 这在Java中是不允许的
Converter<String, Integer> converter = new StringToIntConverter();
int result = converter.convert("42");
System.out.println(result); // 输出: 42
1.10.4 泛型方法
C#和Java都支持泛型方法,但C#的语法更加简洁。
C#示例:
public static class Utilities
{
public static T[] CreateArray<T>(params T[] elements) => elements;
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
}
// 使用示例
int[] numbers = Utilities.CreateArray(1, 2, 3);
Console.WriteLine(string.Join(", ", numbers)); // 输出: 1, 2, 3
int x = 5, y = 10;
Utilities.Swap(ref x, ref y);
Console.WriteLine($"x = {x}, y = {y}"); // 输出: x = 10, y = 5
Java示例:
public class Utilities {
@SafeVarargs
public static <T> T[] createArray(T... elements) {
return elements;
}
public static <T> void swap(T[] arr, int i, int j) {
T temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 使用示例
Integer[] numbers = Utilities.createArray(1, 2, 3);
System.out.println(Arrays.toString(numbers)); // 输出: [1, 2, 3]
Integer[] arr = {5, 10};
Utilities.swap(arr, 0, 1);
System.out.println("x = " + arr[0] + ", y = " + arr[1]); // 输出: x = 10, y = 5
注意,Java不支持引用参数,所以我们不能像C#那样直接交换两个变量的值。
1.10.5 默认值和约束
C#允许我们为泛型类型参数指定默认值,这在Java中是不可能的。
C#示例:
public class DefaultValueExample<T> where T : struct
{
public T GetDefaultValue() => default(T);
}
// 使用示例
var example = new DefaultValueExample<int>();
Console.WriteLine(example.GetDefaultValue()); // 输出: 0
var dateExample = new DefaultValueExample<DateTime>();
Console.WriteLine(dateExample.GetDefaultValue()); // 输出: 1/1/0001 12:00:00 AM
Java示例:
Java没有直接对应的功能,但可以通过反射或其他方式模拟:
public class DefaultValueExample<T> {
private final Class<T> type;
public DefaultValueExample(Class<T> type) {
this.type = type;
}
@SuppressWarnings("unchecked")
public T getDefaultValue() {
if (type.equals(int.class)) return (T) Integer.valueOf(0);
if (type.equals(boolean.class)) return (T) Boolean.FALSE;
// ... 其他基本类型的处理
return null; // 对于引用类型,返回null
}
}
// 使用示例
DefaultValueExample<Integer> example = new DefaultValueExample<>(int.class);
System.out.println(example.getDefaultValue()); // 输出: 0
DefaultValueExample<Boolean> boolExample = new DefaultValueExample<>(boolean.class);
System.out.println(boolExample.getDefaultValue()); // 输出: false
结论
通过这些示例,我们可以看到C#的泛型在某些方面比Java的更加强大和灵活。C#提供了更丰富的类型约束、协变和逆变支持,以及默认值处理等特性。这些特性使得C#在处理复杂的泛型场景时更加得心应手。
对于从Java转向.NET Core的开发者,熟悉这些差异不仅能帮助你更好地理解和使用C#的泛型,还能让你在设计API时充分利用C#的独特优势。记住,虽然基本概念相似,但细节上的差异可能会对代码的结构和性能产生重大影响。
在实际开发中,你会发现C#的泛型能够帮助你编写出更加类型安全、更加灵活的代码。特别是在处理集合、算法实现、依赖注入等场景时,C#的泛型优势会更加明显。
最后,不要忘记C#还有一些其他与泛型相关的高级特性,比如泛型协变和逆变在异步编程中的应用、泛型特化等。这些topics可能需要更深入的学习和实践。继续探索,你会发现C#泛型的更多精彩之处!
1.11 LINQ (Language Integrated Query)
欢迎来到LINQ的世界!对于从Java转向.NET Core的开发者来说,LINQ可能是最令人兴奋的特性之一。LINQ为C#带来了强大的查询能力,使得处理集合和数据变得异常简单和优雅。让我们深入探讨LINQ,并与Java中的相似功能进行比较。
1.11.1LINQ基础
LINQ允许我们使用类似SQL的语法直接在代码中查询数据,无论是内存中的集合、数据库还是XML文档。
C#示例:
var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// 查询语法
var evenNumbers = from num in numbers
where num % 2 == 0
select num;
// 方法语法
var evenNumbersMethod = numbers.Where(num => num % 2 == 0);
Console.WriteLine(string.Join(", ", evenNumbers)); // 输出: 2, 4, 6, 8, 10
Java示例:
Java8引入的Stream API提供了类似的功能,但语法和能力有所不同:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> evenNumbers = numbers.stream()
.filter(num -> num % 2 == 0)
.collect(Collectors.toList());
System.out.println(String.join(", ", evenNumbers.stream().map(Object::toString).collect(Collectors.toList())));
// 输出: 2, 4, 6, 8, 10
1.11.2 复杂查询
LINQ的强大之处在于它能够轻松处理复杂的查询,包括排序、分组和连接操作。
C#示例:
var people = new List<Person>
{
new Person { Name = "Alice", Age = 25, City = "New York" },
new Person { Name = "Bob", Age = 30, City = "London" },
new Person { Name = "Charlie", Age = 35, City = "New York" },
new Person { Name = "David", Age = 40, City = "London" }
};
var query = from person in people
where person.Age > 30
orderby person.Name
group person by person.City into cityGroup
select new
{
City = cityGroup.Key,
Count = cityGroup.Count(),
AverageAge = cityGroup.Average(p => p.Age)
};
foreach (var result in query)
{
Console.WriteLine($"City: {result.City}, Count: {result.Count}, Average Age: {result.AverageAge}");
}
// 输出:
// City: London, Count: 1, Average Age: 40
// City: New York, Count: 1, Average Age: 35
Java示例:
Java的Stream API也可以实现类似的功能,但语法会更加复杂:
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
class Person {
String name;
int age;
String city;
//构造函数、getter和setter省略
}
List<Person> people = Arrays.asList(
new Person("Alice", 25, "New York"),
new Person("Bob", 30, "London"),
new Person("Charlie", 35, "New York"),
new Person("David", 40, "London")
);
Map<String, List<Person>> groupedByCity = people.stream()
.filter(person -> person.age > 30)
.sorted((p1, p2) -> p1.name.compareTo(p2.name))
.collect(Collectors.groupingBy(person -> person.city));
groupedByCity.forEach((city, cityGroup) -> {
double averageAge = cityGroup.stream().mapToInt(p -> p.age).average().orElse(0);
System.out.printf("City: %s, Count: %d, Average Age: %.2f%n", city, cityGroup.size(), averageAge);
});
// 输出:
// City: London, Count: 1, Average Age: 40.00
// City: New York, Count: 1, Average Age: 35.00
1.11.3 延迟执行
LINQ查询默认是延迟执行的,这意味着查询只在结果被实际使用时才执行。这与Java的Stream API类似。
C#示例:
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var query = numbers.Select(n => {
Console.WriteLine($"Processing {n}");
return n * 2;
});
Console.WriteLine("Query defined.");
foreach (var num in query)
{
Console.WriteLine($"Result: {num}");
}
// 输出:
// Query defined.
// Processing 1
// Result: 2
// Processing 2
// Result: 4
// Processing 3
// Result: 6
// Processing 4
// Result: 8
// Processing 5
// Result: 10
Java示例:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = numbers.stream().map(n -> {
System.out.println("Processing " + n);
return n * 2;
});
System.out.println("Stream defined.");
stream.forEach(num -> System.out.println("Result: " + num));
// 输出:
// Stream defined.
// Processing 1
// Result: 2
// Processing 2
// Result: 4
// Processing 3
// Result: 6
// Processing 4
// Result: 8
// Processing 5
// Result: 10
1.11.4 查询提供者
LINQ的一个强大特性是它可以针对不同的数据源使用相同的查询语法。例如,LINQ to SQL允许我们使用LINQ语法直接查询数据库。
C#示例(使用Entity Framework Core):
using (var context = new MyDbContext())
{
var query = from user in context.Users
where user.Age > 18
orderby user.Name
select new { user.Name, user.Age };
foreach (var item in query)
{
Console.WriteLine($"Name: {item.Name}, Age: {item.Age}");
}
}
这个查询会被转换为SQL并在数据库中执行。
Java示例:
Java没有直接等价的功能。JPA(Java Persistence API)提供了类似的查询能力,但语法不同:
EntityManager em = // 获取EntityManager
TypedQuery<User> query = em.createQuery(
"SELECT NEW com.example.UserDTO(u.name, u.age) " +
"FROM User u WHERE u.age >18 ORDER BY u.name", User.class);
List<UserDTO> results = query.getResultList();
for (UserDTO user : results) {
System.out.println("Name: " + user.getName() + ", Age: " + user.getAge());
}
1.11.5 LINQ的其他特性
LINQ还有许多其他强大的特性,例如:
- 即时查询(使用ToList()、ToArray()等方法)
- 自定义查询操作符
- 并行LINQ(PLINQ)用于并行查询处理
C#示例(PLINQ):
var numbers = Enumerable.Range(1, 1000000);
var evenSquares = numbers.AsParallel().Where(n => n % 2 == 0)
.Select(n => n * n);
Console.WriteLine($"Count of even squares: {evenSquares.Count()}");
Java示例(并行流):
import java.util.stream.IntStream;
long count = IntStream.range(1, 1000001)
.parallel()
.filter(n -> n % 2 == 0)
.mapToLong(n -> (long)n * n)
.count();
System.out.println("Count of even squares: " + count);
结论
LINQ是C#中一个非常强大和灵活的特性,它大大简化了数据查询和处理的代码。虽然Java8引入的Stream API提供了类似的功能,但LINQ在语法简洁性和统一性方面仍然具有优势。
对于从Java转向.NET Core的开发者,掌握LINQ将极大地提高你的生产力。LINQ不仅可以用于内存中的集合,还可以无缝地应用于各种数据源,如数据库和XML。
在实际开发中,你会发现LINQ在处理复杂数据查询、数据转换和数据分析时特别有用。它可以帮助你编写更加简洁、可读性更强的代码,同时保持良好的性能。
记住,虽然LINQ强大,但也要注意合理使用。过度复杂的LINQ查询可能会影响代码的可读性和性能。在编写LINQ查询时,始终要考虑查询的效率和可维护性。
最后,随着你对LINQ的深入了解,你会发现它不仅仅是一个查询工具,而是一种强大的编程范式,可以改变你思考和解决问题的方式。继续探索LINQ的高级特性,你会发现更多令人兴奋的可能性!
1.12 异步编程 (async/await)
在现代软件开发中,异步编程已经成为提高应用程序性能和响应性的关键技术之一。C# 中的 async/await 语法提供了一种简洁而强大的方式来处理异步操作,使得编写非阻塞代码变得更加容易。对于从Java 转向 .NET Core 的开发者来说,理解 C# 中的异步编程模型及其与 Java 的差异是非常重要的。让我们深入探讨 C# 的异步编程,并与 Java 进行对比。
异步编程的基础概念
在开始之前,我们需要理解几个关键概念:
- 异步操作:不会立即完成的操作,通常涉及 I/O 或长时间运行的计算。
- 非阻塞:允许程序在等待异步操作完成时继续执行其他代码。
- Task:表示异步操作的对象。
- async/await:C# 中用于简化异步编程的关键字。
C# 中的async/await
C# 的 async/await 模型提供了一种直观的方式来编写异步代码,使其看起来像同步代码。这大大简化了异步编程,提高了代码的可读性和可维护性。
基本语法
public async Task<string> FetchDataAsync()
{
// 模拟异步操作
await Task.Delay(1000);
return "Data fetched!";
}
// 调用异步方法
public async Task UseDataAsync()
{
string result = await FetchDataAsync();
Console.WriteLine(result);
}
在这个例子中:
async
关键字标记方法为异步方法。Task<T>
表示异步操作,其中T
是返回值的类型。await
关键字用于等待异步操作完成,而不阻塞线程。
与 Java 的对比
Java 8 引入了 CompletableFuture 来处理异步操作,但语法和使用方式与 C# 的async/await 有很大不同。让我们看一个 Java 的例子:
public CompletableFuture<String> fetchDataAsync() {
return CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Data fetched!";
});
}
// 调用异步方法
public void useDataAsync() {
fetchDataAsync().thenAccept(result -> System.out.println(result));
}
对比可以看出:
- C# 的 async/await 语法更加直观,代码结构更接近同步代码。
- Java 需要使用 CompletableFuture 和 lambda 表达式,这可能导致代码嵌套和复杂性增加。
- C# 的异常处理更自然,可以直接使用 try-catch,而 Java 通常需要在 CompletableFuture 中处理异常。
深入理解 Task 和 ValueTask
在 C# 的异步编程中,Task 和 ValueTask 是两个核心概念,它们都用于表示异步操作,但有一些重要的区别。
Task
Task 是.NET 中表示异步操作的标准方式。它是一个引用类型,可以表示无返回值的操作(Task)或有返回值的操作(Task)。
public async Task DoSomethingAsync()
{
await Task.Delay(1000);
Console.WriteLine("Task completed");
}
public async Task<int> CalculateAsync()
{
await Task.Delay(1000);
return 42;
}
Task 的优点:
- 可以表示复杂的异步操作。
- 支持取消、延续和异常处理。
- 可以轻松组合多个异步操作。
ValueTask
ValueTask 是一个较新的概念,设计用于优化性能,特别是在异步操作可能立即完成的情况下。它是一个值类型,可以避免在某些情况下分配额外的内存。
public ValueTask<int> GetValueAsync()
{
if (cachedValue.HasValue)
{
return new ValueTask<int>(cachedValue.Value);
}
return new ValueTask<int>(FetchValueAsync());
}
private async Task<int> FetchValueAsync()
{
await Task.Delay(1000);
return 42;
}
ValueTask 的优点:
- 在同步完成的情况下可以避免内存分配。
- 适用于频繁调用且经常同步完成的方法。
何时使用 ValueTask
- 当方法频繁被调用,并且大部分情况下可以同步完成时。
- 当你需要优化性能,减少内存分配时。
注意事项
- ValueTask 不应该被多次使用,这可能导致不可预知的行为。
- Task 更适合长时间运行或复杂的异步操作。
异步编程最佳实践
- 避免异步死锁:不要在同步上下文中等待异步方法,特别是在 UI 线程中。
// 错误示范
private void Button_Click(object sender, EventArgs e)
{
var result = DoSomethingAsync().Result; // 可能导致死锁}
// 正确做法
private async void Button_Click(object sender, EventArgs e)
{
var result = await DoSomethingAsync();
}
-
使用 ConfigureAwait(false):在不需要同步上下文的地方,使用 ConfigureAwait(false) 可以提高性能。
public async Task<string> FetchDataAsync() { var result = await httpClient.GetStringAsync(url).ConfigureAwait(false); return result.ToUpper(); }
-
正确处理异常:使用 try-catch 块来处理异步方法中的异常。
public async Task ProcessDataAsync() { try { var data = await FetchDataAsync(); await ProcessAsync(data); } catch (HttpRequestException ex) { Console.WriteLine($"Failed to fetch data: {ex.Message}"); } catch (Exception ex) { Console.WriteLine($"An error occurred: {ex.Message}"); } }
-
合理使用 Task.WhenAll 和 Task.WhenAny:当需要并行执行多个异步操作时,这些方法非常有用。
public async Task ProcessMultipleAsync() { var task1 = FetchDataAsync("url1"); var task2 = FetchDataAsync("url2"); var task3 = FetchDataAsync("url3"); var results = await Task.WhenAll(task1, task2, task3); foreach (var result in results) { Console.WriteLine(result); } }
-
避免不必要的异步:如果操作是快速完成的,使用同步方法可能更合适。
-
使用异步流:在C# 8.0 及以后版本中,可以使用异步流(IAsyncEnumerable)来处理异步数据序列。
public async IAsyncEnumerable<int> GenerateSequenceAsync() { for (int i = 0; i < 10; i++) { await Task.Delay(100); yield return i; } } public async Task ConsumeSequenceAsync() { await foreach (var item in GenerateSequenceAsync()){ Console.WriteLine(item); } }
结论
C# 的 async/await 模型为异步编程提供了一个强大而直观的工具。相比 Java 的 CompletableFuture,C# 的方法更加简洁和易于理解。Task 和 ValueTask 的引入进一步增强了异步编程的灵活性和性能。
对于从 Java 转向 .NET Core 的开发者来说,掌握这些概念和最佳实践将大大提高编写高效、可维护的异步代码的能力。记住,异步编程不仅仅是about使用正确的关键字,更是关于理解并发和异步操作的本质,以及如何在实际应用中最好地利用它们。
通过不断练习和实践,你将能够充分利用 C# 异步编程的强大功能,创建响应更快、更高效的应用程序。
1.13 异常处理
异常处理是任何编程语言中的关键概念,对于从Java转向.NET Core的开发者来说,理解两种语言在异常处理上的异同点至关重要。虽然C#和Java在异常处理的基本概念上有很多相似之处,但C#提供了一些独特的特性和语法糖,可以使异常处理更加灵活和强大。让我们深入探讨C#的异常处理机制,并与Java进行对比。
1.13.1 基本异常处理结构
C#和Java的基本异常处理结构非常相似,都使用try-catch-finally块。
C#示例:
try
{
// 可能抛出异常的代码
int result = 10 / 0;
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"除零错误:{ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"发生了一个错误:{ex.Message}");
}
finally
{
Console.WriteLine("这里的代码总是会执行");
}
Java示例:
try {
// 可能抛出异常的代码
int result = 10 / 0;
} catch (ArithmeticException ex) {
System.out.println("除零错误:" + ex.getMessage());
} catch (Exception ex) {
System.out.println("发生了一个错误:" + ex.getMessage());
} finally {
System.out.println("这里的代码总是会执行");
}
主要区别:
- C#使用大括号{}来包围try, catch和finally块,而Java使用小括号()。
- C#的异常类型名称略有不同,例如DivideByZeroException而不是ArithmeticException。
- C#使用$进行字符串插值,这是一个更现代和方便的字符串格式化方法。
1.13.2 异常过滤器(C# 6.0+)
C#引入了异常过滤器,这是Java中没有的特性。异常过滤器允许你在catch块中添加额外的条件。
try
{
// 可能抛出异常的代码
}
catch (Exception ex) when (ex.Message.Contains("specific error"))
{
Console.WriteLine("捕获到特定错误");
}
catch (Exception ex)
{
Console.WriteLine($"其他错误:{ex.Message}");
}
这个特性在Java中是不存在的,Java需要在catch块内部使用if语句来实现类似的功能。
1.13.3 使用when关键字进行模式匹配(C# 7.0+)
C# 7.0引入了模式匹配,可以在catch块中使用when关键字结合模式匹配来处理异常。
try
{
// 可能抛出异常的代码
}
catch (Exception ex) when (ex is FileNotFoundException fnf)
{
Console.WriteLine($"文件未找到:{fnf.FileName}");
}
catch (Exception ex) when (ex is UnauthorizedAccessException uae)
{
Console.WriteLine($"访问被拒绝:{uae.Message}");
}
这种方式比Java的instanceof检查更加简洁和强大。
1.13.4 抛出异常
C#和Java在抛出异常方面非常相似,都使用throw关键字。
C#示例:
if (someCondition)
{
throw new ArgumentException("无效的参数");
}
Java示例:
if (someCondition) {
throw new IllegalArgumentException("无效的参数");
}
主要区别在于异常类的名称可能略有不同。
1.13.5 自定义异常
在C#和Java中创建自定义异常的方式也很相似,但C#提供了更多的构造函数选项。
C#示例:
public class CustomException : Exception
{
public CustomException() { }
public CustomException(string message) : base(message) { }
public CustomException(string message, Exception inner) : base(message, inner) { }
protected CustomException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
}
Java示例:
public class CustomException extends Exception {
public CustomException() { }
public CustomException(String message) { super(message); }
public CustomException(String message, Throwable cause) { super(message, cause); }
public CustomException(Throwable cause) { super(cause); }
}
C#的自定义异常类通常包括一个用于序列化的构造函数,这在Java中是不需要的。
1.13.6 异常处理中的性能考虑
C#和Java在异常处理的性能开销上有相似之处,但C#提供了一些独特的优化机会。
- 使用Nullable类型避免异常: C#的Nullable可以用来表示可能不存在的值,而不是抛出异常。
int? result = SomeMethod();
if (result.HasValue)
{
Console.WriteLine(result.Value);
}
else
{
Console.WriteLine("No value");
}
-
使用TryParse模式: C#的许多内置类型提供TryParse方法,可以避免抛出异常。
if (int.TryParse(input, out int number)) { Console.WriteLine($"解析成功:{number}"); } else { Console.WriteLine("解析失败"); }
-
异常过滤器的性能: C#的异常过滤器不会展开堆栈,这可能比在catch块内部进行条件检查更高效。
1.13.7 异步方法中的异常处理
C#在异步方法中处理异常时特别强大,这要归功于async/await语法。
public async Task ProcessAsync()
{
try
{
await SomeAsyncMethod();
}
catch (SpecificException ex)
{
await HandleSpecificExceptionAsync(ex);
}
catch (Exception ex)
{
await LogExceptionAsync(ex);
throw;
}
}
相比之下,Java的CompletableFuture需要使用不同的方法来处理异常:
public CompletableFuture<Void> processAsync() {
return someAsyncMethod()
.exceptionally(ex -> {
if (ex instanceof SpecificException) {
return handleSpecificException((SpecificException) ex);
} else {
logException(ex);
throw new CompletionException(ex);
}
});
}
C#的方法更加直观,并且更容易与同步代码保持一致的结构。
1.13.8 新的C# 8.0特性:Using声明
C# 8.0引入了using声明,这是一种简化资源管理和异常处理的新方法。
public void ProcessFile(string path)
{
using var file = new StreamReader(path);
//文件会在方法结束时自动关闭
// 即使发生异常也是如此
}
这比Java的try-with-resources语句更加简洁。
结论
虽然C#和Java在异常处理的基本概念上有很多相似之处,但C#提供了一些独特的特性,如异常过滤器、模式匹配和更简洁的资源管理语法。这些特性使得C#的异常处理更加灵活和强大。
对于从Java转向.NET Core的开发者来说,熟悉这些差异和新特性将有助于编写更加健壮和高效的代码。记住,好的异常处理不仅仅是捕获和抛出异常,还包括合理使用异常、优化性能以及提高代码的可读性和可维护性。
在实际开发中,建议充分利用C#提供的这些特性,同时保持对性能影响的警惑。适当的异常处理策略可以显著提高应用程序的稳定性和用户体验。继续探索和实践这些概念,你将能够在.NET Core环境中更加自如地处理各种异常情况。
1.14 文件I/O操作
文件I/O操作是几乎所有应用程序中的基本功能。对于从Java转向.NET Core的开发者来说,了解两个平台在文件处理上的异同点非常重要。虽然基本概念相似,但.NET Core提供了一些独特的特性和语法糖,使文件操作更加简洁和高效。让我们深入探讨.NET Core的文件I/O操作,并与Java进行对比。
1.14.1 基本文件操作
读取文件
C#示例:
string content = File.ReadAllText("path/to/file.txt");
// 或者逐行读取
string[] lines = File.ReadAllLines("path/to/file.txt");
// 使用流读取
using var stream = new StreamReader("path/to/file.txt");
string line;
while ((line = stream.ReadLine()) != null)
{
Console.WriteLine(line);
}
Java示例:
String content = new String(Files.readAllBytes(Paths.get("path/to/file.txt")));
// 或者逐行读取
List<String> lines = Files.readAllLines(Paths.get("path/to/file.txt"));
// 使用缓冲读取器
try (BufferedReader reader = new BufferedReader(new FileReader("path/to/file.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
主要区别:
- C#的
File
类提供了更多便捷的静态方法。 - C#使用
using
语句自动管理资源,而Java使用try-with-resources。 - C#的
StreamReader
类似于Java的BufferedReader
。
写入文件
C#示例:
File.WriteAllText("path/to/file.txt", "Hello, World!");
// 追加内容
File.AppendAllText("path/to/file.txt", "Additional content");
// 使用流写入
using var writer = new StreamWriter("path/to/file.txt");
writer.WriteLine("Hello, World!");
writer.WriteLine("Another line");
Java示例:
Files.write(Paths.get("path/to/file.txt"), "Hello, World!".getBytes());
// 追加内容
Files.write(Paths.get("path/to/file.txt"), "Additional content".getBytes(), StandardOpenOption.APPEND);
// 使用缓冲写入器
try (BufferedWriter writer = new BufferedWriter(new FileWriter("path/to/file.txt"))) {
writer.write("Hello, World!");
writer.newLine();
writer.write("Another line");
}
主要区别:
- C#的
File
类方法更加直观。 - Java需要显式指定字符编码和打开选项。
- C#的
StreamWriter
类似于Java的BufferedWriter
。
1.14.2 文件和目录操作
文件操作
C#示例:
// 检查文件是否存在
bool exists = File.Exists("path/to/file.txt");
// 复制文件
File.Copy("source.txt", "destination.txt", overwrite: true);
// 移动文件
File.Move("oldpath.txt", "newpath.txt");
// 删除文件
File.Delete("path/to/file.txt");
Java示例:
// 检查文件是否存在
boolean exists = Files.exists(Paths.get("path/to/file.txt"));
// 复制文件
Files.copy(Paths.get("source.txt"), Paths.get("destination.txt"), StandardCopyOption.REPLACE_EXISTING);
// 移动文件
Files.move(Paths.get("oldpath.txt"), Paths.get("newpath.txt"), StandardCopyOption.REPLACE_EXISTING);
// 删除文件
Files.delete(Paths.get("path/to/file.txt"));
主要区别:
- C#使用静态
File
类方法,而Java使用Files
类的静态方法。 - Java需要显式指定复制和移动的选项。
目录操作
C#示例:
// 创建目录
Directory.CreateDirectory("path/to/new/directory");
// 获取文件列表
string[] files = Directory.GetFiles("path/to/directory");
// 获取子目录列表
string[] subdirectories = Directory.GetDirectories("path/to/directory");
// 删除目录
Directory.Delete("path/to/directory", recursive: true);
Java示例:
// 创建目录
Files.createDirectories(Paths.get("path/to/new/directory"));
// 获取文件列表
try (Stream<Path> paths = Files.list(Paths.get("path/to/directory"))) {
paths.filter(Files::isRegularFile).forEach(System.out::println);
}
// 获取子目录列表
try (Stream<Path> paths = Files.list(Paths.get("path/to/directory"))) {
paths.filter(Files::isDirectory).forEach(System.out::println);
}
// 删除目录
Files.walk(Paths.get("path/to/directory"))
.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
主要区别:
- C#的
Directory
类提供了更直观的方法。 - Java的目录操作更加灵活,但可能需要更多代码。
- C#删除目录可以通过一个方法调用完成,而Java需要遍历删除。
1.14.3 异步文件操作
.NET Core对异步文件操作的支持非常出色,这是相对于Java的一个显著优势。
C#示例:
public async Task ProcessFileAsync(string filePath)
{
string content = await File.ReadAllTextAsync(filePath);
// 处理内容
await File.WriteAllTextAsync("output.txt", processedContent);
}
Java示例:
Java的文件I/O操作主要是同步的。虽然可以使用CompletableFuture
来模拟异步操作,但并不如C#原生支持那么方便:
public CompletableFuture<Void> processFileAsync(String filePath) {
return CompletableFuture.supplyAsync(() -> {
try {
String content = new String(Files.readAllBytes(Paths.get(filePath)));
// 处理内容
Files.write(Paths.get("output.txt"), processedContent.getBytes());
return null;
} catch (IOException e) {
throw new CompletionException(e);
}
});
}
主要区别:
- C#提供了原生的异步文件I/O方法。
- Java需要手动将同步操作包装在
CompletableFuture
中。
1.14.4 文件流和内存流
文件流
C#示例:
using var fileStream = new FileStream("path/to/file.txt", FileMode.Open, FileAccess.Read);
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
// 处理读取的数据
}
Java示例:
try (FileInputStream fis = new FileInputStream("path/to/file.txt")) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
// 处理读取的数据
}
}
内存流
C#示例:
using var memoryStream = new MemoryStream();
byte[] data = Encoding.UTF8.GetBytes("Hello, World!");
await memoryStream.WriteAsync(data, 0, data.Length);
memoryStream.Position = 0;
string result = Encoding.UTF8.GetString(memoryStream.ToArray());
Java示例:
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] data = "Hello, World!".getBytes(StandardCharsets.UTF_8);
baos.write(data);
String result = baos.toString(StandardCharsets.UTF_8);
主要区别:
- C#的
Stream
类提供了异步方法,而Java的流操作主要是同步的。 - C#的
MemoryStream
可以直接重置位置,而Java的ByteArrayOutputStream
需要转换为ByteArrayInputStream
来重新读取。
1.14.5 文件监视
.NET Core和Java都提供了文件系统监视功能,但实现方式略有不同。
C#示例:
using var watcher = new FileSystemWatcher("path/to/directory");
watcher.Created += OnFileCreated;
watcher.Changed += OnFileChanged;
watcher.Deleted += OnFileDeleted;
watcher.Renamed += OnFileRenamed;
watcher.EnableRaisingEvents = true;
// 事件处理方法
private static void OnFileCreated(object sender, FileSystemEventArgs e)
{
Console.WriteLine($"File created: {e.FullPath}");
}
Java示例:
Path path = Paths.get("path/to/directory");
try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE);
while (true) {
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
System.out.println("Event kind: " + event.kind() + ". File affected: " + event.context() + ".");
}key.reset();
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
主要区别:
- C#使用事件驱动模型,而Java使用轮询模型。
- C#的实现更加简洁和直观。
- Java的实现需要显式的循环和异常处理。
1.14.6 文件压缩
.NET Core和Java都提供了文件压缩功能,但API略有不同。
C#示例:
using System.IO.Compression;
// 创建zip文件
using (var zipArchive = ZipFile.Open("archive.zip", ZipArchiveMode.Create))
{
zipArchive.CreateEntryFromFile("file1.txt", "file1.txt");
zipArchive.CreateEntryFromFile("file2.txt", "file2.txt");
}
// 解压zip文件
ZipFile.ExtractToDirectory("archive.zip", "extractPath");
Java示例:
import java.util.zip.*;
// 创建zip文件
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip"))) {
addToZipFile("file1.txt", zos);
addToZipFile("file2.txt", zos);
}
// 解压zip文件
try (ZipInputStream zis = new ZipInputStream(new FileInputStream("archive.zip"))) {
ZipEntry zipEntry = zis.getNextEntry();
while (zipEntry != null) {
String fileName = zipEntry.getName();
File newFile = new File("extractPath/" + fileName);
// 提取文件
// ...zipEntry = zis.getNextEntry();
}
}
private static void addToZipFile(String fileName, ZipOutputStream zos) throws IOException {
File file = new File(fileName);
try (FileInputStream fis = new FileInputStream(file)) {
ZipEntry zipEntry = new ZipEntry(fileName);
zos.putNextEntry(zipEntry);
byte[] bytes = new byte[1024];
int length;
while ((length = fis.read(bytes)) >= 0) {
zos.write(bytes, 0, length);
}
zos.closeEntry();
}
}
主要区别:
- C#提供了更高级的
ZipFile
类,使压缩和解压操作更加简单。 - Java需要手动处理每个zip条目,代码较为冗长。
- C#的API设计更加用户友好,而Java的API提供了更底层的控制。
结论
虽然.NET Core和Java在文件I/O操作的基本概念上有许多相似之处,但.NET Core提供了一些独特的特性和更简洁的API,使得文件操作更加方便和高效。对于从Java转向.NET Core的开发者来说,主要需要注意以下几点:
-
静态方法vs对象方法:C#更倾向于使用静态方法(如
File
和Directory
类),而Java则更多地使用对象方法。 -
异步支持:.NET Core对异步文件I/O的原生支持是一个显著优势,可以更容易地编写高性能的文件操作代码。
-
资源管理:C#的
using
语句和Java的try-with-resources语句都提供了自动资源管理,但C#的语法更加简洁。 -
API设计:总体而言,.NET Core的文件I/O API设计更加直观和用户友好,而Java的API则提供了更多的底层控制。
-
文件监视:C#的事件驱动模型比Java的轮询模型更加简洁和易用。
-
压缩操作:C#提供了更高级的API来处理文件压缩,使得操作更加简单。
在实际开发中,熟悉这些差异将有助于更快地适应.NET Core环境,并充分利用其提供的特性来编写高效、简洁的文件I/O代码。同时,.NET Core的跨平台特性也使得文件操作代码可以在不同的操作系统上运行,提供了更大的灵活性。
随着技术的不断发展,建议持续关注.NET Core在文件I/O方面的新特性和改进,以便在项目中充分利用这些优势。通过实践和深入学习,你将能够在.NET Core环境中更加得心应手地处理各种文件操作任务。
文章部分内容来着AI编写可能存在错误或过期内容,如果有问题请评论
.NET 新人入门技术QQ交流群:157210263