1. 名字重复问题:字段、属性、参数
新手经常遇到属性名、构造函数参数名和私有字段名一样的情况。这种情况下编译器不会报错,但结果可能不是你想要的。
例子
public class Student
{
private string name;
public Student(string name)
{
name = name; // “看起来没毛病...”
}
public void PrintName()
{
Console.WriteLine(name);
}
}
会发生什么?PrintName()方法啥都不会打印:私有字段name还是null!因为在构造函数里name = name;其实只操作了参数,不是类的字段。
怎么避免
用this关键字来访问类的字段/属性:
public Student(string name)
{
this.name = name; // 现在没问题了!
}
小贴士:Rider和其他IDE会帮你发现这种问题——别无视它们的提示!
2. 没有默认构造函数:什么时候需要?
如果你声明了至少一个自己的构造函数,编译器就不会再自动生成“空”的默认构造函数。这经常导致错误,尤其是你想无参数创建对象时。
例子
public class Book
{
public string Title { get; set; }
// 明确声明的构造函数
public Book(string title)
{
Title = title;
}
}
var book = new Book(); // 编译器:“哔哔!没有无参数构造函数!”
如果你想既能无参数又能有参数创建实例,记得手动加上需要的构造函数:
public Book() { }
public Book(string title)
{
Title = title;
}
3. 访问修饰符:私生活的公开人物
新手常犯的错——没考虑访问修饰符。默认情况下,类的字段和方法是private,结构体成员也是private。不写或者忘了写,可能导致你在类外用不了需要的属性或方法。
例子
public class Animal
{
string name; // 默认private
public void PrintName()
{
Console.WriteLine(name);
}
}
var animal = new Animal();
animal.name = "巴尔西克"; // 编译错误!
编译器不会让你访问私有成员。最好明确写上修饰符:
public string Name;
或者
private string name;
更推荐用属性来访问:
public string Name { get; set; }
4. 忘记初始化字段:null陷阱
很常见的坑——类的字段没初始化。尤其是字段会在显式初始化前被方法用到时。
例子
public class Cat
{
public string Name;
public void SayHello()
{
Console.WriteLine($"喵!名字长度{Name.Length}个字!"); // 可能NullReferenceException
}
}
var cat = new Cat();
cat.SayHello(); // 悲剧了!
变量Name等于null,访问Length就会抛NullReferenceException。创建对象后一定要注意它的状态!
建议:用构造函数强制设置重要字段,属性可以标记为必填(新C#用required)。
5. 自动属性无限递归:复制粘贴的锅
有时候手写属性时会写错——比如getter或setter里又访问自己(不是私有字段),结果死循环。
例子
public class Dog
{
public int Age
{
get { return Age; } // 哎呀!
set { Age = value; } // 哎呀呀!
}
}
这种代码每次访问Age都会自己调自己...无限递归(直到栈溢出——递归自虐)。
正确写法
用单独的私有字段:
private int age;
public int Age
{
get { return age; }
set { age = value; }
}
或者直接用自动属性:
public int Age { get; set; }
6. 名字不统一
编程时大小写和命名很重要。C#是区分大小写的(Case-Sensitive),Name、name、NAME是三个不同的标识符。
还有,如果你改了字段或属性名,记得所有地方都要一起改!Rider能帮你重构,但拼写相近时也可能出错。
7. 用错修饰符(static/instance)
新手有时会把实例方法和静态方法搞混。问题出在你想用C#语法不允许的方式访问字段/方法时。
例子
public class MathHelper
{
public static int Add(int a, int b) => a + b;
public int Multiply(int a, int b) => a * b;
}
var sum = MathHelper.Add(2, 3); // OK
var mul = MathHelper.Multiply(2, 3); // 错误!
Multiply不是静态方法,不能用类名直接调,得先new对象:
var helper = new MathHelper();
var mul = helper.Multiply(2, 3); // 这样才对
8. 继承构造函数时的错误
如果你的类继承自别的类,别忘了基类可能要求构造函数传参数。
例子
public class Animal
{
public string Name;
public Animal(string name)
{
Name = name;
}
}
public class Dog : Animal
{
public Dog() { } // 错误:基类要求name!
}
编译器不会让你这样写。要显式调用基类构造函数:
public class Dog : Animal
{
public Dog(string name) : base(name) { }
}
9. 忘了实现接口的所有成员
如果你的类实现了接口,但没实现所有方法——编译就会报错:“类没有完全实现接口”。
例子
public interface IWalker
{
void Walk();
void Run();
}
public class Turtle : IWalker
{
public void Walk()
{
Console.WriteLine("慢慢来,稳稳的...");
}
// 哎呀!Run忘了!
}
IDE会报错,Turtle编译不过,直到你把接口所有方法都实现。
10. 用了没初始化的对象
在C#里对象要用new显式创建。如果你只是声明了引用变量但没赋对象——它就是null。访问字段/方法时就会遇到著名的NullReferenceException。
例子
Student student;
student.PrintName(); // 惊喜!
一定要初始化对象:
Student student = new Student("瓦西丽萨");
student.PrintName();
11. 错误:public字段代替属性
老“反模式”:把类的所有字段都声明成public。为啥不好?字段没保护,无法控制修改,封装性全无!
例子
public class Car
{
public int speed; // 别这样!
}
最好用属性:
public class Car
{
public int Speed { get; set; }
}
属性可以加校验、逻辑、限制:
private int speed;
public int Speed
{
get { return speed; }
set
{
if (value < 0) speed = 0;
else speed = value;
}
}
12. 隐式创建“魔法数字”或字符串
如果你在代码里看到42、"伊万"、"SomeFile.txt"等“魔法”值直接写在方法体里——这是潜在的坑。最好用常量或字段。
例子
public class ConfigManager
{
public void Save()
{
File.WriteAllText("config.txt", "some data"); // 魔法字符串!
}
}
更好的写法:
public class ConfigManager
{
private const string ConfigFileName = "config.txt";
public void Save()
{
File.WriteAllText(ConfigFileName, "some data");
}
}
13. 复制错误:变量引用同一个对象
在C#里,变量-对象其实是引用。把一个变量赋给另一个,只是复制了引用,不是对象本身。这经常导致“突然”变化。
例子
Student s1 = new Student("瓦夏");
Student s2 = s1;
s1.Name = "科利亚";
Console.WriteLine(s2.Name); // 会输出“科利亚”!
为啥?因为s1和s2其实指向同一个对象!
14. 错误时没有反馈
很糟糕的是,类允许赋不合理的值。比如负年龄。
例子
public class Human
{
public int Age { get; set; }
}
var h = new Human();
h.Age = -50; // 没问题...
类的逻辑应该防止无意义的值:
private int age;
public int Age
{
get { return age; }
set
{
if (value < 0)
throw new ArgumentException("年龄不能为负。");
age = value;
}
}
15. 作用域混乱(scope)
有时候变量在不同作用域里重名——方法里和类字段一样。
例子
public class MyClass
{
int value = 10;
public void Foo()
{
int value = 99;
Console.WriteLine(value); // 会输出99,不是10!
}
}
要明确访问类字段,用this.value。
16. 重写方法时忘了override关键字
你想重写基类的虚方法时,别忘了override!
例子
public class Animal
{
public virtual void Speak() => Console.WriteLine("动物说话");
}
public class Cat : Animal
{
public void Speak() => Console.WriteLine("喵!"); // 不是override!
}
这里基类方法没被重写:新方法只是隐藏了老的(shadowing),用基类引用时可能出意外。
正确写法:
public override void Speak() => Console.WriteLine("喵!");
17. 在构造函数里调用虚方法
这不算编译错误,但经常导致难查的bug。如果在基类构造函数里调用虚方法,而这个方法在子类被重写——调用时子类字段可能还没初始化。
例子
public class Animal
{
public Animal()
{
Speak();
}
public virtual void Speak()
{
Console.WriteLine("动物...");
}
}
public class Cat : Animal
{
public string Name { get; set; } = "巴尔西克";
public override void Speak()
{
Console.WriteLine($"喵!我是{Name}");
}
}
Cat cat = new Cat();
// 调用Speak()时Cat构造函数还没执行完,Name还是null!
18. 只用构造函数描述对象(没有属性)
如果类只有构造函数,创建后对象就不能改——适合不可变对象,但大多数时候最好让类的使用者能改属性(至少部分)。
19. 忽略标准命名规范(naming conventions)
类名首字母大写,方法也是,字段小写加下划线(或camelCase),常量全大写。统一风格不仅让代码更好读,还能减少低级错误!
20. 没用到的字段和方法
如果你声明了字段但没用过——那就是“死代码”。IDE会高亮这些。死角越少,项目越好维护!
GO TO FULL VERSION