类和结构/扩展方法

类和结构

类和结构都是创建对象的模板,每个对象都包含数据,并提供了处理和访问数据的方法,类定义类的每个对象(实例)可以包含什么数据和功能,类和机构的区别在于他们在内存中的存储方式/访问方式,类是存储在堆(heap)上的引用类型,结构是存储在栈(stack)上的值类型,并且结构不支持继承,从语法上来看结构与类非常相似,主要区别是结构使用关键字struct声明,而类使用class声明,类和对象将琐碎的C#知识点系统的组合到一起,构成一个完整的程序。

类中的数据和函数称为类的成员,成员的可访问性可以是public/protected/internal protected/private/internal

数据成员

数据成员是包含类的数据,其中包括了字段/常量/事件成员(事件是类的成员,在发生某些行为时,可以让对象通知调用方,"事件处理程序"),数据成员可以是静态数据,类成员总是实例成员,除非使用static显式声明

函数成员

函数成员提供了操作类中数据的某些功能,包括方法/属性/构造函数/析构函数/运算符/索引器   

方法

方法是与类相关的函数,,默认为实例成员,使用static可以定义静态方法

方法声明

在 C#中 ,方法的定义包括任意方法修饰符(如方法的可访问性)、返回值的类型,然后依次是方法名和输入参数的列表,和方法体。每个参数都包括参数的类型名和在方法体中的引用名称。 如果方法有返回值,return语句就必须与返回值一起使用

语法
[modifiers] return_type MethodName([parameters])
{
      // Method body
}
 示例
class Test
{
    /// <summary>
    /// 实例方法
    /// </summary>
    public void Show()
    {
        Console.WriteLine("Test下实例方法");
    }

    /// <summary>
    /// 静态方法属于类本身
    /// </summary>
    public static void Show2()
    {
        Console.WriteLine("Test下静态方法");
    }
}
方法调用

在实例化类得到类的对象后,即可通过对象“.”方法名称进行方法的调用,需要注意的是使用static修饰的方法属于类的本身,无法使用类的实例化对象进行调用,使用类名“.”方法名即可

实例方法调用
Test test = new Test();
test.Show();
静态方法调用
Test.Show2();
命名参数

参数一般需要按定义的顺序传送给方法,命名参数允许按任意顺序传递

class Program
{
    static void Main(string[] args)
    {
        //下面两种调用方式结果是一样的
        Print("hello", "world");
        Print(b: "world", a: "hello");
        Console.ReadKey();
    }
    static void Print(string a, string b)
    {
        Console.WriteLine("{0},{1}", a, b);
    }
}
可选参数

参数也可以是可选的,必须为可选参数提供默认值,可选参数还必须是方法定义的最后一个参数

static void Print(string a, string b = "world")
{
    Console.WriteLine("{0},{1}", a, b);
}
Print("hello");//输出hello,world
Print("hello","wang");//输出hello,wang

方法的重载

方法名相同,但参数的个数或类型不同

static void Print(string a)
{
    Console.WriteLine("{0}", a);
}
static void Print(string a, string b = "world")
{
    Console.WriteLine("{0},{1}", a, b);
}

注意:

  • 两个方法不能仅在返回类型上有区别
  • 两个方法不能仅根据参数是声明为ref还是out来区分

属性

属性property的概念是:它是一个方法或一对方法,在客户端代码看来,它(们)是一个字段

定义属性
public string SomeProperty
{
    get
    {
        return "This is the property value";
    }
    set
    {
        //type string
    }
}
只读和只写属性

在属性定义中省略set访问器,就可以创建只读属性;同样,在属性定义中省略get访问器,就可以创建只写属性

class Program
{
    private string age;
    private string name;
    //只读属性
    public string Age
    {        
        get
        {
            return age;
        }           
    }
    //只写属性
    public string Name
    {        
        set
        {
            value= name;
        }           
    }
}
属性的访问修饰符

C#允许给属性的getset访问器设置不同的访问修饰符,所以属性可以有公有的get访问器和私有或受保护的set访问器。这有助于控制属性的设置方式或时间

自动实现的属性

如果属性的setget访问器中没有任何逻辑,就可以使用自动实现的属性,这种属性会自动实现后备成员变量,使用自动实现的属性,就不能在属性设置中验证属性的有效性

class Program
{
    public string Age { get; set; }
    public string Name { get; set; }
    static void Main(string[] args)
    {
        Console.ReadKey();
    }
}

构造函数

声明基本构造函数的语法就是声明一个与包含的类同名的方法,但该方法没有返回类型

构造函数声明
class Person
{
    Person()
    {

    }
}

其实没有必要给类提供构造函数,原因在于如果没有在类中没有提供任何构造函数,编译器会在后台创建一个默认的无参构造函数用来把所有的成员字段初始化为标准的默认值

构造函数重载

构造函数重载遵循与其他方法相同的规则,就是说,允许为构造函数提供任意多的重载,但是需要注意:如果提供了带参数的构造函数,编译器就不会隐式的自动创建默认的构造函数

class Program
{
    static void Main(string[] args)
    {
        //调用无参构造函数实例化对象
        Person person1 = new Person();

        //调用带参数的构造函数实例化对象
        Person person2 = new Person(3);

        //因为带name参数的构造函数是private的,所以这里无法实例化
        //Person person3 = new Person(20, "wang");
        Console.ReadKey();
    }
}
class Person
{
    private int age;

    private string name;
    public Person()
    {

    }
    public Person(int age)
    {
        //使用this关键字区分成员字段和同名参数
        this.age = age;
    }

    private Person(int age, string name)
    {
        this.age = age;
        this.name = name;
    }
}
class Program
{
    static void Main(string[] args)
    {
        //调用无参构造函数实例化对象
        Person person1 = new Person();

        //调用带参数的构造函数实例化对象
        Person person2 = new Person(3);

        //因为带name参数的构造函数是private的,所以这里无法实例化
        //Person person3 = new Person(20, "wang");
        Console.ReadKey();
    }
}
class Person
{
    private int age;

    private string name;
    public Person()
    {

    }
    public Person(int age)
    {
        //使用this关键字区分成员字段和同名参数
        this.age = age;
    }

    private Person(int age, string name)
    {
        this.age = age;
        this.name = name;
    }
}
class Person
{
    private int age;

    private string name;       
    private Person(int age, string name)
    {
        this.age = age;
        this.name = name;
    }
}

这个例子没有为Person类定义任何公有的或受保护的构造函数。这就使Person不能使用new运算符在外部代码中实例化(但可以在Person中编写一个公有静态属性或方法,以实例化该类)。 这在下面两种情况下是有用的:

  • 类仅用作某些静态成员或属性的容器,因此永远不会实例化它
  • 希望类仅通过调用某个静态成员函数来实例化(单例模式)
构造函数初始化器

有时,在一个类中有几个构造函数,以容纳某些可选参数,这些构造函数包含一些共同的代码。需要做到从构造函数中调用其他构造函数就可以使用构造函数初始化器

class Program
{
    static void Main(string[] args)
    {
        Person person1 = new Person(20);

        Person person2 = new Person(20, "li");

        Console.WriteLine("person1:age={0},name={1}", person1.age, person1.name);
        Console.WriteLine("person2:age={0},name={1}", person2.age, person2.name);

        Console.ReadKey();
    }
}
class Person
{
    public int age;

    public string name;

    public Person(int age)
    {
        this.age = age;
        this.name = "wang";
    }

    public Person(int age, string name)
    {
        this.age = age;
        this.name = name;
    }
}

上面的例子是一个简单的构造函数重载,然后通过调用不同的构造函数实例化对象,Person类的两个构造函数初始化了相同的字段age,显然,最好把所有的代码放在一个地方。
C#中使用构造函数初始化器,可以实现此目的

class Program
{
    static void Main(string[] args)
    {
        Person person1 = new Person(20);

        Person person2 = new Person(20, "li");

        Console.WriteLine("person1:age={0},name={1}", person1.age, person1.name);
        Console.WriteLine("person2:age={0},name={1}", person2.age, person2.name);

        Console.ReadKey();
    }
}
class Person
{
    public int age;

    public string name;

    public Person(int age)
    {
        this.age = age;
        this.name = "wang";
    }

    public Person(int age, string name) : this(age)
    {
        this.age = age;
        this.name = name;
    }
}

这里this关键字仅调用参数最匹配的那个构造函数。 注意,构造函数初始化器在构造函数的函数体之前执行。C#构造函数初始化器可以包含对同一个类的另一个构造函数的调用(使用前面介绍的语法),也可以包含对直接基类的构造函数的调用(使用相同的语法,但应使用base关键字代替this。初始化器中不能有多个调用

静态类

如果类只包含静态的方法和属性,该类就是静态的。静态类在功能上与使用私有静态函数创建的类相同,不能创建静态类的实例。

静态构造函数

静态构造函数只执行一次,而前面的构造函数是实例构造函数,只要创建类的对象,就会被执行。编写静态构造函数的一个原因是,类有一些静态字段或属性,需要在第一次使用类之前,从外部源中初始化这些静态字段和属性

注意:.Net运行库没有确保什么时候执行静态构造函数,所以不应把要求在某个特定时刻(例如,加载程序集时)执行的代码放在静态构造函数中。也不能预计不同类的静态构造函数按照什么顺序执行。但是,可以确保静态构造函数至多运行一次,即在代码引用类之前调用它。在C#中,通常在第一次调用类的任何成员之前执行静态构造函数。静态构造函数没有访问修饰符,其他C#代码从来不调用它,但在加载类时,总是由.NET运行库调用它,所以像publicprivate这样的访问修饰符就没有任何意义。出于同样原因,静态构造函数不能带任何参数,一个类也只能有一个静态构造函数。很显然,静态构造函数只能访问类的静态成员,不能访问类的实例成员。

无参数的实例构造函数与静态构造函数可以在同一个类中同时定义。尽管参数列表相同,但这并不矛盾,因为在加载类时执行静态构造函数,而在创建实例时执行实例构造函数,所以何时执行哪个构造函数不会有冲突。如果多个类都有静态构造函数,先执行哪个静态构造函数就不确定。此时静态构造函数中的代码不应依赖于其他静态构造函数的执行情况。 另一方面,如果任何静态字段有默认值,就在调用静态构造函数之前指定它们。

结构

结构是值类型

结构是会影响性能的值类型,但根据使用结构的方式,这种影响可能是正面的,也可能是负面的。 正面的影响是为结构分配内存时,速度非常快,因为它们将内联或者保存在栈中。 在结构超出了作用域被删除时,速度也很快。负面影响是,只要把结构作为参数来传递或者把一个结构赋予另一个结构(如A-B,其 中A和 B是结构),结构的所有内容就被复制,而对于类,则只复制引用。 这样就会有性能损失,根据结构的大小,性能损失也不同。注意,结构主要用于小的数据结构。但当把结构作为参数传递给方法时,应把它作为 ref参数传递,以避免性能损失,此时只传递了结构在内存中的地址,这样传递速度就与在类中的传递速度工样快了。但如果这样做,就必须注意被调用的方法可以改变结构的值。

结构不支持继承

结构(和C#中的其他类型一样)最终派生于类system.Object因此,结构也可以访问system.Object的方法。在结构中,甚至可以重写system.Object中的方法—如重写Tostring()方法。 结构的继承链是:每个结构派生自system.ValueType类 ,system,ValueType类又派生自system.Object。 ValueType并没有给Object添加任何新成员,但提供了一些更适合结构的实现方式。注意,不能为结构提供其他基类:每个结构都派生自ValueType

结构的构造函数

为结构定义构造函数的方式与为类定义构造函数的方式相同,但不允许定义无参数的构造函数。.Net运行库禁止在C#结构内定义无参的构造函数

类和结构的区别

结构与类的区别是它们在内存中的存储方式、访问方式(类是存储在堆上的引用类型,而结构是存储在栈上的值类型)和它们的一些特征(如结构不支持继承。较小的数据类型使用结构可提高性能。但在语法上,结构与类非常相似,主要的区别是使用关键字struct代替class来声明结构。对于类和结构,都使用关键字new来声明实例:这个关键字创建对象并对其进行初始化

匿名类型

匿名类型只是一个继承自Object且没有名称的类。该类的定义从初始化器中推断,类似于隐式类型化的变量。

var person = new { Name = "wang", Age = 22 };

运算符重载

重载运算符是具有特殊名称的函数,是通过关键字operator后跟运算符的符号来定义的。与其他函数一样,重载运算符有返回类型和参数列表。

class Program
{
    static void Main(string[] args)
    {
        Person p1 = new Person { Age = 1, Name = "wang" };
        Person p2 = new Person { Age = 2, Name = "li" };
        Person p3 = p1 + p2;
        Console.WriteLine(p3.Age);
        Console.WriteLine(p3.Name);
        Console.ReadKey();
    }
}
class Person
{
    public int Age { get; set; }
    public string Name { get; set; }
    /// <summary>
    /// 重载“+”号运算符,实现两个Person对象的相加
    /// </summary>
    /// <param name="a"></param>
    /// <param name="b"></param>
    /// <returns></returns>
    public static Person operator +(Person a, Person b)
    {
        Person person = new Person();
        person.Age = a.Age + b.Age;
        person.Name = a.Name + b.Name;
        return person;
    }
}

输出:
3
wangli

上面只是简单的举个例子,需要注意的是:1:不是所有的运算符都支持重载,2:比较运算符需要成对的重载,如果重载了==,则也必须重载!=,否则产生编译错误。

扩展方法

有许多扩展类的方式。 如果有类的源代码,继承是给对象添加功能的好方法。但如果没有源代码,该怎么办?此时可以使用扩展方法,它允许改变一个类,但不需要该类的源代码。扩展方法是静态方法,它是类的一部分,但实际上没有放在类的源代码中。

class Program
 {
     static void Main(string[] args)
     {
         Person person = new Person();
         //调用Person类的扩展方法
         person.UpdatePerson("wang", 3);
         Console.WriteLine(person.Name);
         Console.WriteLine(person.Age);
         Console.ReadKey();
     }
 }
 static class Extension
 {
     public static void UpdatePerson(this Person person, string name, int age)
     {
         person.Name = name + ",Hello";
         person.Age += age;
     }
 }
 class Person
 {
     public int Age { get; set; }
     public string Name { get; set; }

     /// <summary>
     /// 重载“+”号运算符,实现两个Person对象的相加
     /// </summary>
     /// <param name="a"></param>
     /// <param name="b"></param>
     /// <returns></returns>
     public static Person operator +(Person a, Person b)
     {
         Person person = new Person();
         person.Age = a.Age + b.Age;
         person.Name = a.Name + b.Name;
         return person;
     }
 }

注意:扩展方法是静态方法;如果扩展方法与类中的某个方法同名,就从来不会调用扩展方法。类中已有的任何实例方法优先

Object类

之前说到,所有的.Net类都派生自System.Object,如果在定义时没有指定基类编译器就会假定该类派生自Object,实际意义在于除了使用自己定义的方法和属性外,还可以访问Object类定义的许多公有的受保护的成员方法,比如ToString()/GetHashCode()/Equals()/ReferenceEquals()/Finalize()

给方法传递参数详解

值类型和引用类型

C#中的类型一共分为两类,一类是值类型Value Type,一类是引用类型(Reference Type),值类型包括结构体struct和枚举enum,引用类型包括类class、接口interface、委托delegate、数组array

常见的简单类型如short、int、long、float、double、byte、char等其本质上都是结构体,对应struct、System.Int16、System.Int32、System.Int64、System.Single、System.Double、Syetem.Byte、System.Char,因此它们都是值类型。但string和object例外,它们本质上是类,对应class System.String和System.Object,所以它们是引用类型

值类型

值类型变量本身保存了该类型的全部数据,当声明一个值类型的变量时,该变量会被分配到栈Stack

引用类型

引用类型变量本身保存的是位于堆Heap上的该类型的实例的内存地址,并不包含数据。当声明一个引用类型变量时,该变量会被分配到栈上。如果仅仅只是声明这样一个变量,由于在堆上还没有创建该类型的实例,因此,变量值为null,意思是不指向任何类型实例(堆上的对象)。对于变量的类型声明,用于限制此变量可以保存的类型

值传递和引用传递

C#函数的参数如果不加ref,out这样的修饰符显式声明参数是通过引用传递外,默认都是值传递

注意
参数的类型是值类型还是引用类型传参数时用值传递还是引用传递是两个不同的概念

值传递参数

此时传递是原变量的拷贝,值传递参数和原变量的内存地址不同,因此方法中对值传递参数的修改不会改变原变量

static void IntParam(int value)
{
    value = 10;
}
int intValue = 5;
IntParam(intValue);

 变量intValueint值类型,系统为变量intValue在堆栈Stack上分配的内存地址为:0x001ff3f8,该内存地址存储的数据值为5

 

当调用IntParam方法时,由于是值传递,系统会为局部参数变量value在堆栈Stack上分配一个新的内存区域,内存地址为:0x001ff3a8,并将intValue中的数据值 5复制到局部参数变量value中。这里可以看到局部参数变量value和原变量intValue的内存地址是不同的

对局部参数变量value的赋值操作是对变量内存地址中的数据值进行修改,此处是将局部参数变量value的内存地址0x088aecd0中的数据值由原来的5改为10

 

结果:因为原变量intValue和局部参数变量value并不是同一块内存区域(内存地址不同),所以IntParam方法中对局部参数变量value的赋值操作不会影响到原变量intValueintValue的值在调用IntParam方法前后是相同的。实际上,方法内发生的更改只影响局部参数变量value

注意:这里的参数类型换成string或者object结果是一样的,如果不加ref/out默认都是值传递

看下面的例子

static void ChangeArr1(int[] values)
{
    values = new int[3] { 4, 5, 6 };
}
static void ChangeArr2(int[] values)
{
    values[0] = 999;
}
int[] arr = new int[] { 1, 2, 3 };
ChangeArr1(arr);            
Console.WriteLine(string.Join(",", arr));

 一般认为值传递就是把值拷贝一份,然后不管在函数中对传入的参数做啥改变,参数之前的值不会受啥影响,所以arr没有变成4,5,6,仍然是1,2,3

int[] arr = new int[] { 1, 2, 3 };
ChangeArr2(arr);            
Console.WriteLine(string.Join(",", arr));

 

这里arr[0]为什么变成999了?

前面我们有说到引用类型在内存中是保存为两个部分,一个是stack中的内存地址,另一个是heap中的实际值.用时我们只直接用stack中的值,假设stack中的值为0xabcdefgh ,arr指向它. 那么我们按值传递时就是把这个stack的值拷贝成另一份arr也指向它

 

但是我们操作内存地址这样的值时不会像整数一样直接操作它,而只会通过它去找heap中的实际值.

于是我们arr[0] =999.改变了实际上还是heap中数组的值了. 但arr = new int []{4,5,6}没有对之前传的arr产生影响.这个操作的意义是在heap中重新开辟一块内存,保存着值1,2,3. 这块内存的地址赋给arr,于是它之前的值0xabcdefgh被改写了.但arr指的值stack值仍没变,仍是0xabcdefgh

引用传递参数

引用传递参数是原变量的指针,引用传递参数和原变量的内存地址相同,相应的方法中对引用传递参数的修改会改变原变量

ref参数

如前所述,通过值传送变量是默认的,也可以使值参数通过引用传送给方法。为此,要使用ref关键字如果把一个参数传递给方法,且这个方法的输入参数前带有ref关键字,则该方法对变量所做的任何改变都会影响原始对象的值。

 

 

out 参数

在方法的输入参数前面加上out前缀时,传递给该方法的变量可以不初始化。该变量通过引用传递,所以在从被调用的方法中返回时,对应方法对该变量进行的任何改变都会保留下来。输出参数函数必须在函数内部进行初始化赋值

class Program
{
    static void Main(string[] args)
    {
        string value;//并未对变量value进行初始化
        Print(out value);
        Console.WriteLine("value:{0}", value);

        Console.ReadKey();
    }

    static string Print(out string value)
    {
        return value = "b";
    }
}

refout参数的区别

首先:两者都是按地址传递的,使用后都将改变原来参数的数值。
其次:ref可以把参数的数值传递进函数,但是out是要把参数清空,就是说你无法把一个数值从out传递进去的,out进去后,参数的数值为空,所以你必须初始化一次。这个就是两个的区别,或者说就像有的网友说的,ref是有进有出,out是只出不进

参考
https://blog.csdn.net/lsl277879661/article/details/56481650
https://blog.csdn.net/weiwenhp/article/details/7644790
https://www.csdndoc.com/article/461002

原文地址:https://www.cnblogs.com/GnailGnepGnaw/p/10601664.html