[C#]代码之坑–编译器不会告诉你的那些事

There is nothing permanent except change. 唯变化永恒不变
-[英]赞格威尔

You have to pay for your decision. 你所做的一切决定,都是有代价的,或是性能,或是灵活性,没有最优解,你需要自己去权衡。
暴王

这些一个个的零散的小点都是在日常工作中总结出来的,似乎没有哪一本编程书会讲这些,所以就总结出来放到这里

小心循环

不要在循环里写循环体公用的初始化方法,应该在循环外只初始化一次
Wrong:

foreach(string t in items)
{
   List nameList = something.GetList();
   if(nameList.Contains(t))
       ...
}

上面例子可以看到,每次循环都需要请求一次GetList(),并且获取到的List不会被更改,因此每次循环时nameList都是一样的,这时应该把GetList()方法的调用提出循环体
Correct:

List nameList = something.GetList();
foreach(string t in items)
{
   if(nameList.Contains(t))
       ...
}

传值 Vs 传引用 Vs 传副本 Vs 传求值方法 Vs 私有字段

传值

优点: 简单
缺点: 任何更改对原值不起作用

string a = "a";
public void Dosomething(string a)
{
    a = "b"
}
Console.WriteLine(a);
string a = "aaaaa";
string b = a.Replace('a', 'b');
Console.WriteLine(a);

传引用

优点: 更改会对原值起作用,一次改动可以影响所有引用该对象的地方
缺点: 不注意会引起副作用

object c = new object();
object d = c;
d = null;
Console.WriteLine(c == null);
public class Test
 {
        public string Field1 { get; set; }
        public string Field2 { get; set; }

        public Test(string field1, string field2)
        {
            Field1 = field1;
            Field2 = field2;
        }   
        
        public void Dosomething(Test f)
        {
            f.Field1 = "3";
            f = null;
            f = new Test("5", "6");
        }
 }
  
 Test e = new Test("1", "2");
 e.Dosomething(e);
 Console.WriteLine(e == null);
 Console.WriteLine(e.Field1); ???????

传副本

优点: 保护了源数据不被更改
缺点: 序列化反序列化可能造成性能问题

public class ProtectObject 
{
    private object valuableObject;
    public object ValuableObject
    {
        get
        {
            var objectCopy = JsonSerializer.Serialize(valuableObject);
            return JsonSerialize.Deserialize<object>(objectCopy);
        }
    }
}

求值方法

优点: 每次都能得到最新的值
缺点: 每次都重新计算值,可能造成性能问题

class ContactInfo
{
    public string Phonenumber;
    public string Name;
    public Address Address;
    public string AddressStr 
    {
        get 
        {
            return string.Format("{0} {1} {2} {4}", City, State, Address1, Address2);
        }
    }
}

私有字段

优点: 方便
缺点: 不好追踪值是在什么时候被改掉的

空对象模式

通过对缺失对象的封装,以提供默认无任何行为的对象替代品。

namespace NullObjectPattern.Implementation1
{
  public interface ILog
  {
    void Write(string message);
  }


public class ConsoleLog : ILog
  {
    public void Write(string message)
    {
      Console.WriteLine(message);
    }
  }



public class NullLog : ILog
  {
    public void Write(string message)
    {
      // do nothing
    }
  }



public class Client
  {
    public void TestCase1()
    {
      ILog log1 = new ConsoleLog();
      ILog log2 = new NullLog();
      log1.Write("message to log");
      log2.Write("message to log");
    }
  }
}

封装字段 & 封装对象

数据完整性
Wrong:

class ContactInfo
{
    public string Address 1;
    public string Address 2;
    public string State;
    public string City;
    public string Phonenumber;
    public string Name;
    public GetAddressString()
    {
        return string.Format("{0} {1} {2} {4}", City, State, Address1, Address2);
    }
}

Right:

class Address
{
    public string Address 1;
    public string Address 2;
    public string State;
    public string City;
}
class ContactInfo
{
    public string Phonenumber;
    public string Name;
    public Address Address;
    public string AddressStr 
    {
        get 
        {
            return string.Format("{0} {1} {2} {4}", City, State, Address1, Address2);
        }
    }
}

封装集合(mutable)

public class TestCollection
    {
        public List NumberList { get; private set; }
        public readonly List ReadOnlyNumberList;
    }
 public TestCollection()
    {
        NumberList = new List();
        ReadOnlyNumberList = new List();
    }
    
  TestCollection t1 = new TestCollection();
  t1.NumberList = new List();
  t1.ReadOnlyNumberList = new List&lint>();
  t1.NumberList.Add(1);
  t1.ReadOnlyNumberList.Add(2);

卫语句

函数中的条件逻辑使人难以看清正常的执行途径。使用卫语句表现所有特殊情况。

动机:条件表达式通常有2种表现形式。第一:所有分支都属于正常行为。第二:条件表达式提供的答案中只有一种是正常行为,其他都是不常见的情况。

这2类条件表达式有不同的用途。如果2条分支都是正常行为,就应该使用形如if…..else…..的条件表达式;如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。这样的单独检查常常被称为“卫语句”。卫语句常用于数据校验,并且上面的逻辑“保卫”了下面的逻辑。

void func(void)  
{  
    if(IsWorkingDay())  
   {  
       printf("Error,is working day");  
   }  
    else  
   {  
       if(IsWorkingTime())  
      {  
        printf("Error ,is working time");  
       }  
       else  
      {  
          rest();  
       }  
    }  
}  

使用卫语句

void func()  
{   
   if(IsWorkingDay())  
   {  
      printf("Error,is work day");  
      return;  
   }  
   
   if(IsWorkingTime())  
   {  
      printf("Error,is work time");  
      return ;  
   }  
  
   rest();  
} 

保持接口稳定

IContact
{
    SaveContactInfo(int id, string name, string phoneNumber)
    GetContactInfoById(int id)
}

增加了地址

Wrong:

IContact
{
    SaveContactInfo(int id, string name, string phoneNumber, string address1, string address2, string city, string state)
    GetContactInfoById(int id)
}

Right:

IContact
{
    SaveContactInfo(ContactInfo contact)
    GetContactInfoById(int id)
}
class ContactInfo
{
    public int Id;
    public string Name;
    public string PhoneNumber;
    public Address AddressInfo;
}
class Address
{
    public string Address1;
    public string Address2;
    public string City;
    public string State;
}

readonly 关键字的局限

我们在日常编程中,如果想指定某个property或field不被外界修改,通常会使用readonly关键字,这样外界就不能对这个字段赋值了,但是对于对象或者集合来说,这个关键字虽然保证了property或者field本身不能被赋值,但是外部依旧可以修改对象中的property或者field或者修改集合,我们来看下面的代码
readonly
我们有个AClass类,它有一个readonly的对象AObject,和一个readonly的集合List,当我们在外部调用它时会发现,我们不能够直接对AObject或者List赋值,但是我们可以向List添加元素或者修改AObject的属性值。

Solution
对于集合,.net framework提供了readonly的封装,有很多以IReadOnlyXXX开头的集合接口,

IReadOnlyList
IReadOnlyCollection
IReadOnlyDictionary

因此我们要做两件事来达到集合不被修改的目的

  1. 用IReadOnlyXXX接口来声明property,
  2. 声明property为private set
IReadOnlyList ReadOnlyList 
{ get; private set; }

对于对象,参考上面的传副本部分

博主为你专属推荐

被遗忘的 Where

在代码库里发现了类似这样的代码:

class AObject
{
    public AObject(string str)
    { this.AString = str; }
    public string AString;
}
List list = new List() { new AObject("a"), new AObject("b"), new AObject("c") };
list.Where(o => o.AString == "a");

问题:

list.Count?

答案是 3,因为 Where 操作不改变集合本身。如果想用 Where 的结果应该拿 Where 方法的返回值:

list = list.Where(o => o.AString == "a");

Select的误用

在代码库里发现了这样的代码:

class AObject
{
    public AObject(string str)
    { this.AString = str; }
    public string AString;
}
List list = new List() { new AObject("a"), new AObject("b"), new AObject("c") };
list.Select(o => 
{
    o.AString = "d"
    return o;
});

问题:

list[0].AString?

答案是 “d”,因为 list 里面的元素是引用类型 AObject,Select 应该只做属性的选择或类型转换,不应该改变原集合里面元素的属性,如果要改变元素集合的属性,应该用 Each

博主开通了微信公众号,欢迎关注啦

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.