普通视图

发现新文章,点击刷新页面。
今天 — 2025年11月29日首页

C#彻底搞懂属性(Property)与字段(Field)

作者 烛阴
2025年11月28日 09:50

一、字段(Field)

字段是类或结构中直接声明的变量。它的唯一使命,就是作为对象状态的一部分,存储数据。

1. 字段的声明

public class Student
{
    // 这就是两个字段
    public string Name; // 学生姓名
    public int Age;     // 学生年龄
}

2. 访问修饰符 -- Public

Student stu = new Student();
stu.Name = "张三";
stu.Age = -10; // 灾难!年龄怎么可能是负数?

Console.WriteLine($"{stu.Name}的年龄是 {stu.Age}岁。");
// 输出: 张三的年龄是 -10岁。
  • 使用Public可以在任何时候,任何地方直接访问,并且修改,这会存在某些隐患

3. 访问修饰符 -- Private

  • Getter/Setter方法模式
public class Student
{
    // 将字段设为private,外界无法直接访问
    private string _name;
    private int _age;

    // 为了让外界能设置和获取值,我们提供一对公开的方法
    public string GetName()
    {
        return _name;
    }

    public void SetName(string name)
    {
        // 可以在这里加入验证逻辑
        if (string.IsNullOrWhiteSpace(name))
        {
            Console.WriteLine("姓名不能为空!");
            return;
        }
        _name = name;
    }

    public int GetAge()
    {
        return _age;
    }

    public void SetAge(int age)
    {
        // 验证逻辑!
        if (age < 0 || age > 150)
        {
            Console.WriteLine("年龄不合法!");
            return;
        }
        _age = age;
    }
}

// === 使用 ===
Student stu = new Student();
stu.SetName("李四");
stu.SetAge(-10); // 输出: 年龄不合法!
stu.SetAge(25);

Console.WriteLine($"{stu.GetName()}的年龄是 {stu.GetAge()}岁。");

二、属性(Property)

属性是字段的自然演进。它为私有字段提供了一个公开的、受控的访问层,但在语法上,它看起来就像一个公有字段。它巧妙地结合了方法的灵活性和字段的简洁性。

1. 属性的完整声明

一个完整的属性包含一个私有的字段和一对get/set访问器(Accessor)

public class Student
{
    // 1. 私有字段,用于实际存储数据
    private int _age;

    // 2. 公开属性,作为外界访问的入口
    public int Age
    {
        // 3. get访问器:当读取属性时执行
        get
        {
            // 可以加入逻辑,比如权限检查
            return _age;
        }

        // 4. set访问器:当给属性赋值时执行
        set
        {
            // 'value'是一个上下文关键字,代表赋过来的值
            if (value < 0 || value > 150)
            {
                // 可以抛出异常,这是更推荐的做法
                throw new ArgumentOutOfRangeException(nameof(Age), "年龄必须在0到150之间。");
            }
            _age = value;
        }
    }
}

// === 使用属性 ===
Student stu = new Student();
try
{
    stu.Age = 25; // 调用set访问器,value为25
    Console.WriteLine(stu.Age); // 调用get访问器

    stu.Age = -10; // 调用set访问器,value为-10,抛出异常
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

三、属性的进阶 - 语法糖

C#语言在不断发展,属性的写法也越来越简洁。

1. 自动实现属性(Auto-Implemented Properties)

在很多情况下,我们的属性不需要任何特殊的验证逻辑,只是简单地存取一个值。为这种情况手写字段和get/set代码块显得很冗余。于是,C# 引入了自动属性。

public class Product
{
    // 这就是自动属性
    // 编译器会自动在幕后创建一个私有的、匿名的字段
    public string Name { get; set; }
    public decimal Price { get; set; }
}

// === 使用 ===
Product phone = new Product();
phone.Name = "Phone X"; // 背后调用了自动生成的set
phone.Price = 999.99m;

这是我们日常开发中最常用的形式。简洁、高效。只有当你需要添加自定义逻辑时,才需要退回到手动实现字段的方式。

2. 控制可访问性

我们可以为getset访问器设置不同的访问级别。

public class Order
{
    // Id只能在类的内部被设置(比如在构造函数中)
    // 但可以在任何地方被读取
    public int Id { get; private set; }

    public DateTime OrderDate { get; } // 只有get,这是一个只读属性

    public Order(int id)
    {
        this.Id = id; // 在内部设置是合法的
        this.OrderDate = DateTime.UtcNow; // 只读属性只能在构造函数或声明时赋值
    }
}

3. 表达式主体属性

对于一些只读的、由其他数据计算得出的属性,C# 提供了更简洁的写法。

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    // 这个只读属性的值是计算出来的
    public string FullName => $"{FirstName} {LastName}";
}

四、C#中的特殊属性

1. init访问器

init 访问器让属性拥有了特殊的“只写一次”能力。它允许属性在对象初始化器中被赋值,但在那之后就变成只读。

public class Book
{
    public string Isbn { get; init; } // 注意是init
    public string Title { get; init; }
}

// === 使用 ===
var book = new Book
{
    Isbn = "978-0321765723", // 合法!在对象初始化器中赋值
    Title = "The C# Programming Language"
};

// book.Title = "New Title"; // 编译错误!初始化完成后,不能再修改

2. required修饰符

创建对象时必须为某个属性赋值。

public class User
{
    public int Id { get; set; }
    public required string Username { get; set; } // 必须在创建时提供值
    public string? Email { get; set; } // 可选
}

// === 使用 ===
// var user1 = new User(); // 编译错误!Username是required的,但没有被赋值。
var user2 = new User { Username = "alice" }; // 正确

结语

点个赞,关注我获取更多实用 C# 技术干货!如果觉得有用,记得收藏本文!

从`new()`到`.DoSomething()`:一篇讲透C#方法与构造函数的终极指南

作者 烛阴
2025年11月29日 09:50

一、构造函数(Constructor)

构造函数是一个特殊的方法,它的唯一使命就是在创建一个类的实例时执行初始化操作。它确保了对象在被使用之前,处于一个有效的、可预期的初始状态。

1. 构造函数的特征

  1. 名称必须与类名完全相同。
  2. 没有返回类型,甚至连void都不能写。
  3. 通常被声明为public,以便外部代码可以创建类的实例。

2. 默认构造函数

如果你在类中不定义任何构造函数,C#编译器会为你提供一个隐藏的、无参数的默认构造函数

public class Robot
{
    public string Model;
}

// === 使用 ===
Robot r1 = new Robot(); // 编译器提供的默认构造函数被调用
// 此时 r1.Model 的值是其类型默认值,即 null

但只要你定义了任何一个构造函数,编译器就不会再为你提供默认构造函数了。

3. 带参数的构造函数

它强制调用者在创建对象时,必须提供必要的初始数据。

public class Robot
{
    public string Model { get; } // 设置为只读,体现其一旦设定就不应改变的特性
    public DateTime ProductionDate { get; }

    // 这是一个带参数的构造函数
    public Robot(string model)
    {
        // 验证输入
        if (string.IsNullOrWhiteSpace(model))
        {
            throw new ArgumentException("机器人型号不能为空。");
        }

        this.Model = model;
        this.ProductionDate = DateTime.UtcNow; // 记录生产日期

        Console.WriteLine($"型号为 {this.Model} 的机器人已生产!");
    }
}

// === 使用 ===
Robot terminator = new Robot("T-800"); // 必须提供型号
// Robot r2 = new Robot(); // 编译错误!因为定义了有参构造,默认的无参构造消失了。

4. 构造函数重载

一个类可以有多个构造函数,只要它们的参数列表不同即可。

参数列表不同可以是:

  • 类型不同
  • 数量不同
  • 顺序不同
public class Robot
{
    public string Model { get; }
    public string Owner { get; set; }

    // 主构造函数,逻辑最完整
    public Robot(string model, string owner)
    {
        this.Model = model;
        this.Owner = owner;
    }

    // 重载1:只提供型号,主人默认为 "Cyberdyne Systems"
    public Robot(string model)
    {
        this.Model = model;
        this.Owner = "Cyberdyne Systems";
    }
}

5.构造函数链 (this关键字)

上面的重载代码有重复(this.Model = model;)。当初始化逻辑很复杂时,这种重复会导致维护困难。我们可以使用this关键字,让一个构造函数去调用同一个类中的另一个构造函数。

public class Robot
{
    public string Model { get; }
    public string Owner { get; set; }

    // 主构造函数
    public Robot(string model, string owner)
    {
        this.Model = model;
        this.Owner = owner;
    }

    // 使用 : this(model, "Cyberdyne Systems")
    // 表示在执行这个构造函数的函数体之前,
    // 先去调用那个匹配签名的构造函数 Robot(string, string)
    public Robot(string model) : this(model, "Cyberdyne Systems")
    {
        // 这里可以留空,或者只写真正属于这个构造函数的特殊逻辑
        Console.WriteLine("一个无主机器人被生产...");
    }
}

6. 静态构造函数

普通构造函数在new对象时执行,用于初始化实例成员。而静态构造函数在类首次被访问时(如创建第一个实例、或调用静态成员)由.NET运行时自动调用,且只执行一次,用于初始化静态成员

public class RobotFactory
{
    // 静态字段
    private static readonly string _factoryLocation;

    // 静态构造函数
    static RobotFactory()
    {
        // 用于初始化静态数据,比如从配置文件读取信息
        _factoryLocation = "California";
        Console.WriteLine("机器人总工厂启动!只启动一次。");
    }
}

// === 使用 ===
var f1 = new RobotFactory(); // 首次访问类,静态构造函数执行
var f2 = new RobotFactory(); // 不再执行静态构造函数

二、方法(Method

1. 方法的基本语法

// 访问修饰符 返回类型 方法名(参数列表)
// {
//     方法体...
// }
public void Walk(int steps)
{
    Console.WriteLine($"机器人向前走了 {steps} 步。");
}
  • 返回类型:如果方法执行完毕后需要返回一个结果,就指定其类型(int, string, bool等)。如果不需要,就使用void
  • 参数列表:定义了调用该方法时需要传入的数据。

2. 方法重载

与构造函数一样,方法也可以被重载。只要方法名相同,但参数列表不同即可。

public class Calculator
{
    public int Add(int a, int b)
    {
        return a + b;
    }

    // 重载:接受三个整数
    public int Add(int a, int b, int c)
    {
        return a + b + c;
    }

    // 重载:接受两个双精度浮点数
    public double Add(double a, double b)
    {
        return a + b;
    }
}

3.参数的特殊标识:out, ref, params

  1. out参数:返回多个值 out参数用于从方法中传出数据。它要求方法内部必须为其赋值。

    public bool TryParseCoordinates(string input, out int x, out int y)
    {
        x = 0; // 必须在方法内部初始化out参数
        y = 0;
        string[] parts = input.Split(',');
        if (parts.Length == 2 && int.TryParse(parts[0], out x) && int.TryParse(parts[1], out y))
        {
            return true;
        }
        return false;
    }
    
    // === 使用 ===
    string data = "10,20";
    if (TryParseCoordinates(data, out int lat, out int lon))
    {
        Console.WriteLine($"解析成功: X={lat}, Y={lon}");
    }
    
  2. ref参数:按引用传递 默认情况下,值类型(如int, struct)参数是按值传递的(复制一份)。使用ref可以让方法直接操作原始变量

    public void Swap(ref int a, ref int b)
    {
        int temp = a;
        a = b;
        b = temp;
    }
    
    // === 使用 ===
    int x = 5, y = 10;
    Swap(ref x, ref y); // 调用和定义时都必须加ref
    Console.WriteLine($"x={x}, y={y}"); // 输出: x=10, y=5
    
  3. params参数:可变数量的参数 params允许你向方法传入任意数量的同类型参数。它必须是方法参数列表中的最后一个。

    public int Sum(params int[] numbers)
    {
        int total = 0;
        foreach (int num in numbers)
        {
            total += num;
        }
        return total;
    }
    
    // === 使用 ===
    int sum1 = Sum(1, 2, 3);
    int sum2 = Sum(5, 10, 15, 20, 25);
    

三、静态方法static

static关键字既可以修饰字段/属性,也可以修饰方法和构造函数。它划出了一条清晰的界线:属于对象实例的,还是属于类本身的。

  • 实例方法(无static:

    • 属于某个具体的对象
    • 必须通过对象实例来调用。
    • 可以访问该对象的实例成员和静态成员。
  • 静态方法(有static:

    • 属于类本身,不属于任何具体对象。
    • 必须通过类名来调用。
    • 不能访问任何实例成员(因为它不知道你想操作哪个对象),只能访问其他静态成员。
public class Robot
{
    public string Model { get; } // 实例成员
    public static int TotalRobotsProduced { get; private set; } // 静态成员

    public Robot(string model)
    {
        this.Model = model;
        TotalRobotsProduced++; // 实例构造函数可以访问静态成员
    }

    // 实例方法
    public void AnnounceModel()
    {
        // 可以访问实例成员Model和静态成员TotalRobotsProduced
        Console.WriteLine($"我是 {this.Model}。目前共生产了 {TotalRobotsProduced} 台机器人。");
    }

    // 静态方法
    public static void PrintFactoryInfo()
    {
        // 不能访问 this.Model (编译错误)
        Console.WriteLine($"这是一个机器人制造工厂。已生产 {TotalRobotsProduced} 台机器人。");
    }
}

// === 使用 ===
Robot.PrintFactoryInfo(); // 通过类名调用静态方法

Robot r1 = new Robot("R2-D2");
r1.AnnounceModel(); // 通过实例调用实例方法

Robot r2 = new Robot("C-3PO");
r2.AnnounceModel();

Robot.PrintFactoryInfo(); // 静态成员的值被所有实例共享和更新

结语

点个赞,关注我获取更多实用 C# 技术干货!如果觉得有用,记得收藏本文!

昨天以前首页

从`new`关键字开始:精通C#类与对象

作者 烛阴
2025年11月27日 09:55

一、类 -- class关键字

1. 声明一个类

在C#中,我们使用 class 关键字来定义一个类,也就是创建一份“蓝图”。

// 这就是一份最简单的“汽车蓝图”
// public 是一个访问修舍符,意味着这个蓝图在任何地方都可以被看到和使用
public class Car
{
    // 类的成员将在这里定义...
}

恭喜你!你已经创建了你的第一个类。虽然它现在空无一物,但它已经是一个合法的C#类型了。

2. 添加类的成员

  1. 字段(Fields):数据的存储 字段是类内部用来存储数据的变量。通常,为了保护数据,我们会将字段声明为 private

    public class Car
    {
        // 私有字段,用于存储汽车的颜色和当前速度
        // 就像是内部零件,不对外直接暴露
        private string _color;
        private int _speed;
    }
    
  2. 方法(Methods):定义行为的“操作指南” 方法是定义类能做什么的函数。它们是对象的行为。

    public class Car
    {
        // ... 字段 ...
    
        // 公开的方法,定义了汽车的行为
        public void Accelerate()
        {
            _speed += 10;
            Console.WriteLine($"加速!当前速度: {_speed} km/h");
        }
    
        public void Brake()
        {
            _speed -= 10;
            if (_speed < 0) _speed = 0;
            Console.WriteLine($"刹车!当前速度: {_speed} km/h");
        }
    }
    

二、对象实例化 - new关键字

我们有了蓝图,现在需要根据它来建造一辆真正的汽车。这个过程叫做实例化(Instantiation)。我们使用 new 关键字来创建类的实例,也就是对象。

// 声明一个 Car 类型的变量 myCar
// 并使用 new 关键字创建一个 Car 类的实例(对象),然后将其赋给 myCar
Car myCar = new Car();

// 再创建一辆车
Car yourCar = new Car();

// 现在,myCar 和 yourCar 就是两个独立的 Car 对象
// 它们都拥有自己的 _color 和 _speed 字段
// 调用 myCar 的方法,不会影响 yourCar
myCar.Accelerate(); // 输出: 加速!当前速度: 10 km/h
myCar.Accelerate(); // 输出: 加速!当前速度: 20 km/h

yourCar.Brake();    // 输出: 刹车!当前速度: 0 km/h

Console.WriteLine(myCar); // 输出类似: YourProjectName.Car

myCaryourCar是两个引用,它们分别指向内存中两个不同的Car对象。每个对象都有自己独立的一套字段。


三、 类的剖析 - 深入理解核心成员

1. 构造函数(Constructors)

当你使用 new Car() 时,你实际上是在调用一个特殊的方法——构造函数。它的作用是在对象被创建时进行初始化工作,比如为字段设置初始值。

  • 构造函数没有返回类型,连void都没有。
  • 它的名字必须与类名完全相同。
public class Car
{
    private string _color;
    private int _speed;

    // 这是一个构造函数
    // 它在 new Car("Red") 时被调用
    public Car(string color)
    {
        // this 关键字代表当前正在被创建的对象实例
        this._color = color;
        this._speed = 0; // 设定初始速度为0
        Console.WriteLine($"一辆{_color}的汽车被制造出来了!");
    }

    // ... 方法 ...
}

// === 使用构造函数创建对象 ===
Car redCar = new Car("红色");   // 输出: 一辆红色的汽车被制造出来了!
Car blueCar = new Car("蓝色"); // 输出: 一辆蓝色的汽车被制造出来了!

如果你不提供任何构造函数,C#编译器会自动为你生成一个无参数的、空的默认构造函数

2. 属性(Properties)

我们之前将字段设为 private,这是封装原则的体现。但外界如何安全地读取或修改这些数据呢?答案是属性

属性看起来像字段,但内部包含了getset访问器,允许你编写逻辑来控制数据的读写。

public class Car
{
    private string _color;
    private int _speed;

    // 为 _speed 字段创建一个名为 Speed 的公开属性
    public int Speed
    {
        // get 访问器:当读取属性值时执行
        get
        {
            return _speed;
        }
        // set 访问器:当给属性赋值时执行
        set
        {
            // 'value' 是一个关键字,代表赋过来的值
            if (value < 0)
            {
                Console.WriteLine("速度不能为负数!");
            }
            else
            {
                _speed = value;
            }
        }
    }

    // ... 构造函数和方法 ...
}

// === 使用属性 ===
Car myCar = new Car("银色");
myCar.Speed = 50; // 调用 set 访问器,将 50 赋给 value
Console.WriteLine($"当前速度: {myCar.Speed}"); // 调用 get 访问器

myCar.Speed = -10; // 调用 set 访问器,触发验证逻辑
// 输出: 速度不能为负数!

自动属性(Auto-Implemented Properties) 如果你的属性不需要任何特殊的get/set逻辑,C#提供了简洁的语法:

// 编译器会自动在后台创建一个名为<Color>k__BackingField的私有字段
public string Color { get; set; }

四、高级概念 - 深入类的本质

1. static:属于类,而非对象

默认情况下,类的成员(字段、方法等)都是实例成员,每个对象都有一份独立的副本。但有时,我们需要一些成员是所有对象共享的,或者说,是属于类本身的。这时,我们使用 static 关键字。

public class Car
{
    // 静态字段:所有Car对象共享这一个字段
    public static int NumberOfCarsProduced = 0;

    public string Color { get; set; }

    public Car(string color)
    {
        this.Color = color;
        NumberOfCarsProduced++; // 每制造一辆车,就让共享的计数器加一
    }

    // 静态方法:可以直接通过类名调用,无需创建对象
    public static void DisplayProductionInfo()
    {
        Console.WriteLine($"总共生产了 {NumberOfCarsProduced} 辆汽车。");
    }
}

// === 使用静态成员 ===
Car.DisplayProductionInfo(); // 输出: 总共生产了 0 辆汽车。

Car car1 = new Car("红色");
Car car2 = new Car("黑色");

Car.DisplayProductionInfo(); // 输出: 总共生产了 2 辆汽车。

2. 值类型(Value Types)与引用类型(Reference Types)

  • 引用类型(class:

    • 变量存储的是一个引用(地址),指向堆(Heap)内存中对象的实际位置。
    • 将一个引用类型变量赋给另一个,只会复制引用,它们指向同一个对象。
    Car carA = new Car("红色");
    Car carB = carA; // 复制引用,carA 和 carB 指向同一个对象
    
    carB.Color = "蓝色";
    Console.WriteLine(carA.Color); // 输出: 蓝色
    
  • 值类型(struct, int, double, bool等):

    • 变量直接存储数据本身,通常在栈(Stack)内存中。
    • 将一个值类型变量赋给另一个,会创建数据的完整副本
    int a = 10;
    int b = a; // 复制值,a 和 b 是两个独立的 10
    
    b = 20;
    Console.WriteLine(a); // 输出: 10
    

五、类的组织与关系

1. 命名空间(Namespaces)

当项目变大,类的数量增多,可能会出现命名冲突。命名空间就像姓氏一样,用于组织和区分同名的类。

namespace MyCarFactory.Luxury
{
    public class Car { /* ... */ }
}

namespace MyCarFactory.Utility
{
    public class Car { /* ... */ }
}

2. 继承(Inheritance)

一个类可以从另一个类派生,获得其所有非私有的成员。这是OOP的另一大支柱,我们在此简单提及。

// SportsCar 继承自 Car
public class SportsCar : Car
{
    public void ActivateTurbo()
    {
        // ...
    }
}

结语

点个赞,关注我获取更多实用 C# 技术干货!如果觉得有用,记得收藏本文

❌
❌