在学习C#
之前,已经有了一定的python
基础并自制了python
入门手册。学C#
后,发现C#
与python
有较大的区别:很多设计、思路等不能理解,在不断的思索中最终找到了困扰我的最大因素:类型
在python
中,我们可以轻易的写出如下代码:
def add(x,y):
return x*y
但是在C#
中,我们需要写成这样
xxxxxxxxxx
public float add(float x,float y){
return x*y
}
到此就可以引出我们的核心问题:类型
python
属于动态类型语言,类似的有js、php
等,而C#、Java
等属于静态类型语言,二者有一个很大的区别在于:
声明一个对象/变量时需要明确的告诉编译器,这个对象/变量的类型
这一点看似很容易理解,但涉及的因素很多,要深入理解起来需要相当的思考,后续所有内容都围绕着“类型”来展开
学习C#
或Java
的朋友们一定听说过“多态”这个词,多态指的是同一种类型的不同表现状态,我们上代码:
xxxxxxxxxx
namespace 多态
{
class brid
{
public virtual void shout()
{
Console.WriteLine("I am brid");
}
}
class maque : brid
{
public override void shout()
{
Console.WriteLine("I am maque");
}
}
class xique : brid
{
public override void shout()
{
Console.WriteLine("I am xique");
}
}
class Program
{
static void Main(string[] args)
{
brid xiaomaque = new maque();
brid xiaoxique = new xique();
brid xiaoniao = new brid();
xiaomaque.shout();
xiaoxique.shout();
xiaoniao.shout();
}
}
}
代码里,我们先定义了一个 类型名叫brid
,这个brid
的类型里有一个shout
方法
之后有两个子类:麻雀(maque
)和喜鹊(xique
)分别继承了brid
,并重写了父类的shout
方法
之后我们生成对象的时候,分别生成了xiaomaque
和xiaoxique
,但是指向了其父类的类型brid
最终我们实现了同样是brid
类型的xiaomaque
和xiaoxique
,他们属于同一个类型(brid
),但方法执行内容却不同。
这就是多态,同一种类型的不同状态,可是其意义在哪里呢?
我们将上述代码中,Program
里面的内容改成这样:
x class Program
{
static void dosomething(brid b)
{
b.shout();
}
static void Main(string[] args)
{
brid xiaomaque = new maque();
brid xiaoxique = new xique();
brid xiaoniao = new brid();
dosomething(xiaomaque);
dosomething(xiaoxique);
dosomething(xiaoniao);
}
}
这段改动后的结果和改动前输出的内容是一样的,但这段代码里我们新增了一段内容:
xxxxxxxxxx
static void dosomething(brid b)
当某些时候,函数需要传入一个对象,之后执行这个对象的某些方法时,同样需要确定传入对象的类型
在上述例子中,dosomething
函数我们传入了一个 brid
类型的对象b
,执行了b
的shout
方法,多态的存在让我们在传入同一个类型的不同对象时,函数的方法可以变化,这是多态的意义。
但我们之所以强调多态,其根本在于:静态语言必须确定对象的类型!,多态多态,相同类型有不同的状态
我们可以感受下,如果是python
这种动态类型语言
xxxxxxxxxx
class bird:
def shout(self):
print("I am bird")
class maque(bird):
def shout(self):
print("I am maque")
class xique(bird):
def shout(self):
print("I am xique")
def dosomething(b):
b.shout()
dosomething(bird())
dosomething(maque())
dosomething(xique())
在代码中可以看出,def dosomething(b)
时,动态语言的特性决定了不需要告诉IDE
对象的类型,所以这里 的b
可以是任意一个对象,所以说python
天生支持多态,甚至说python
都不存在“态”,使用起来比静态语言要灵活很多
但是动态语言的缺陷在于:
1、动态语言不需要告诉IDE
对象的类型,所以IDE
也无法获知你这个对象是什么类型的,所以在def dosomething(b)
之后,输入b.
时,IDE
无法智能提示对象b
的方法(b
有无数个可能)
2、动态语言的执行效率低,动态的灵活性的背后是无数个静态类型的推测
3、有些问题编译时无法检查错误,运行时发现错误,一直有运行隐患
4、可阅读性差,动态语言没有类型,所以很容易混肴,如果加过多注释又和“简单”的出发点有些背道而驰
所以,静态语言更加严肃,更适合我们学习
当前,用接口的方式是作为多态的一种主流,此处不在讨论
我们看一段python
代码
xxxxxxxxxx
def amazing(func,x):
return func(x)
def power(x):
return x**2
print(amazing(power,6))
在代码中我们可以看到,函数本身可以作为另一个函数的参数,也就是我们常说的回调函数
在动态语言中,因为不需要声明类型,所以可以写的很随意,但是在静态语言中,函数参数里必须声明类型,那么问题来了:
一个函数怎么变成类型呢?
在C#
里,我们采用的方法叫做委托,可以理解为把函数委托成一个类型。
我们看一段C#
代码
xxxxxxxxxx
namespace 委托
{
delegate float Mydelegate(float x);
class Program
{
static float amazing(Mydelegate m,float x)
{
return m(x);
}
static float power(float x)
{
return (float) Math.Pow(x, 2);
}
static void Main(string[] args)
{
Console.WriteLine(amazing(power, 6));
}
}
}
在上面代码中,我们首先定义了一个委托类型,类型名为Mydelegate
,因为委托委托,委托的是一个函数,所以需要有返回值和参数,在上例中,Mydelegate
的返回值为float
,参数为float x
,也就是说,所有返回值为float
,参数为float x
的函数都可以委托到这个类型上
在使用过程中,有一个函数amazing
,他的参数分别是Mydelegate
类型的对象m
以及floa
t的对象x
,并在函数体里执行委托成Mydelegate
类型对象的函数,并将float x
进行传递
在最后有一个power
函数,这个函数符合Mydelegate
的返回值及参数要求,所以可以将power
通过委托的方式传递给amazing
简述:上面例子中,power
函数被委托成了Mydelegate
类型的对象m
,注意:
a、m是一个对象(实例),其类型是Mydelegate
,所以被 作为参数传递
b、此时m()=power()
,m=power
,注意()
c、所有符合Mydelegate
委托定义要求的函数均可以被委托进去
.net
提供了一个已经定义好的 action
委托,其无返回值,无参数,相当于
public delegate void action()
无需我们再定义,可以直接使用
xxxxxxxxxx
namespace ConsoleApp6
{
class Program
{
static void test(Action a)
{
a();
}
static void printf()
{
Console.WriteLine("hello");
}
static void Main(string[] args)
{
test(printf);
}
}
}
上面代码中,定义test
时,参数为Action a
,这里的Action
即.ne
t已经定义好的委托类型
委托的高级应用需要结合泛型,我们马上进入
我们在看C#
代码的时候,经常会碰到这么个符号 <>
, 这个符号一定要攻克下来,彻底明白
因为C#
等静态类型语言,需要声明类型,所以有时候我们还会面对一些问题,比如:
有一个容器,里面存放的是一种类型的对象并作简单处理,当对象很多时,我们需要相应的写很多个容器
泛型的出现,使我们定义好一种容器并贴上标签,需要放入什么样的类型对象时,注明标签即可
(注:多态是同一类型不同状态,泛型这里因存储对象的类型差距较大,通常只能做容器,方法里是对象的一些通用方法)
我们贴一段代码
xxxxxxxxxx
public class GenericClass
{
public void ShowInt(int n)
{
Console.WriteLine("ShowInt print {0},ShowInt Parament Type Is {1}",n,n.GetType());
}
public void ShowDateTime(DateTime dt)
{
Console.WriteLine("ShowDateTime print {0},ShowDateTime Parament Type Is {1}", dt, dt.GetType());
}
public void ShowPeople(People people)
{
Console.WriteLine("ShowPeople print {0},ShowPeople Parament Type Is {1}", people, people.GetType());
}
}
上面的GenericClass
就是一种容器,里面根据不同的类型定义了多种方法,每种方法都操作存放对象的相关信息
为了简化编码过程,泛型登场了
xxxxxxxxxx
public class GenericClass
{
public void ShowInt<T>(T t)
{
Console.WriteLine("ShowInt print {0},ShowInt Parament Type Is {1}",t,t.GetType());
}
}
public void ShowInt<T>
这里,就声明了我后面要用一个T的类型,但是这个类型我还步确定,当使用时再指明类型
GenericClass().ShowInt<int>(2)
xxxxxxxxxx
namespace 泛型
{
public class GenericClass
{
public void ShowInt<T>(T t)
{
Console.WriteLine("ShowInt print {0},ShowInt Parament Type Is {1}", t,t.GetType());
}
}
class Program
{
static void Main(string[] args)
{
new GenericClass().ShowInt<int>(2);
}
}
}
上面的方法为泛型方法,同理,还有泛型类
xxxxxxxxxx
namespace 泛型
{
public class GenericClass
{
public void ShowInt<T>(T t)
{
Console.WriteLine("ShowInt print {0},ShowInt Parament Type Is {1}", t,t.GetType());
}
}
class Program
{
static void Main(string[] args)
{
new GenericClass().ShowInt<int>(2);
new Faning<int>().Dot();
}
}
public class Faning<T>
{
public T t;
public void Dot()
{
Console.WriteLine(t.GetType());
}
}
注意,泛型
想想,如果是python
等动态语言,因为不需要声明类型,所以:
public void ShowInt<T>(T t)
用python
表示 def ShowInt(t)
就无需泛型这样特别处理了,当然,动态类型的缺点前面已经说过
注意:因泛型是可以传入完全不同的类型,所以泛型使用的内部,不能指定某个类型的自有方法(不同类型,无法判断)
所以泛型通常当容器等使用。
泛型除了用作容器外,用作委托也是天造之和
我们前面已经介绍过了自定义委托以及.net
自带委托,我们先从自定义委托说起
委托结合泛型:
xxxxxxxxxx
namespace 泛型委托
{
public delegate void Mydelegate<T>(T t);
class Program
{
static void dosomething(Mydelegate<string> m,string str)
{
m(str);
}
static void Main(string[] args)
{
dosomething(Console.WriteLine, "hello");
}
}
}
上面代码中,我们先定义了一个泛型委托 Mydelegate
,其返回值为void
,其参数有一个,为T
型的对象t
在使用时,我们指定了string
,所以此时所有返回值为void
,参数为string
类型对象的函数皆可被委托进去
然后将Console.WriteLine
这个函数(返回值为空,参数为string
)委托进去,成功执行了m(str)
有了泛型委托,那么对于一个参数的函数来说,无论是int、float
还是任意类型,无需再分别声明委托,定义一个泛型委托即可,在使用时指定类型,减少了代码量(定义委托)。
.net为了方便,提供了Action的泛型委托Action
xxxxxxxxxx
namespace 泛型委托
{
//public delegate void Mydelegate<T>(T t);
class Program
{
static void dosomething(Action<string> m,string str)
{
m(str);
}
static void Main(string[] args)
{
dosomething(Console.WriteLine, "hello");
}
}
}
为了满足泛型需要,泛型委托Action
中可以多达16个参数,也有具备返回值多参数的泛型委托Func
所以,我们基本上不用自定义委托了,.net已经把各种可能定义好了,我们只需要使用时声明泛型的类型即可
泛型还有另一种用途是反射,通常会在泛型函数里,函数里执行了GetType
等操作,在asp.net core
等框架中被大量使用
很多初学者,包含我本人在内,对于类型的理解不够深入,通常我们写的类里面,包含几个数字及字符串,很少类中有其他类,这是因为我们的经验少,还需要不断的学习
此部分的思考从一个循环 嵌套开始:
xxxxxxxxxx
namespace 类中类
{
class ren
{
public dongwu chongwu;
public string name;
public ren(string name)
{
this.name = name;
}
}
class dongwu
{
public string name;
public ren zhuren;
public dongwu(ren r,string name)
{
this.name = name;
this.zhuren = r;
r.chongwu = this;
}
}
class Program
{
static void Main(string[] args)
{
ren laokou = new ren("laokou");
dongwu mimi = new dongwu(laokou,"mimi");
Console.WriteLine(laokou.name);
Console.WriteLine(mimi.name);
Console.WriteLine(laokou.chongwu.name);
Console.WriteLine(mimi.zhuren.name);
Console.WriteLine(laokou.chongwu.zhuren.name);
Console.WriteLine(mimi.zhuren.chongwu.name);
}
}
}
开始思考:
1、类的字段里经常性包含另一个类型,且这个类型不是string、int
等常见类型,而是我们自定义的类型
比如asp.net core
中 httpcontext
里有request
类型的对象,其对象里又有heder
等类型的对象,很多层嵌套,一定要脱离只包含基本类型这种思维(封装)
2、脱离类型与实例之间限制在一种维度的局限性思想,非常重要!
xxxxxxxxxx
class Pet
{
}
class people
{
Pet pet;
}
如上面所例,以往些代码时,在一个类型里包含另一个类型时(本例中为Pet),通常会把字段命名为和类型相近的名称,从思路上理解就是:我有一个宠物类型的 对象叫 **宠物。
从真实世界理解,我们要跨域到另一个思路:我有一个宠物,他的类型是动物,这一点非常重要,仔细理解,完全跨越以前的思维
xxxxxxxxxx
class Dongwu
{
}
class people
{
Dongwu pet;
}
此种思维可能存在的问题是:people().pet
我们下意识会认为是一个pet
类型,但其实际是一个Dongwu
类型,所以即便我们不能很好的使用,但我们一定要知道这种用法的广泛存在,具体是否应用这种方法灵活掌握。
另外,在IDE
中输入people().pet.
时,智能提示的方法是定义pet
字段时其类型的方法(上例中为Dongwu
),可不要误解为pet
是Pet
类型,继承自Dongwu
(此误解很少发生,此处纯属提示)
注:上面的people().pet
中的people()
表示一个people
的对象,在C#中必须用new关键字,即 new people().pet
否则报错,后同
3、字段与类型的区别
我们知道,访问一个对象的属性时,用A.b
这种方式,在A
类的声明过程中,b
是A
的一个字段,但是b
可以定义为任何类型
xxxxxxxxxx
class A
{
Anytype b;
}
但是A.b.
方法里面包含的是已经定义好的b
字段的类型的方法(上面代码中为anytype
包含的方法)
(此内容第2条已写,此处再重复提示)
4、尝试按真实世界去命名对象!
我们在定义A.b
字段时,b
的类型已经确定(定义时需要指定类型),但是b
这个名字应该怎么起?
尝试按真实世界的角度去命名b的名字,比如 Ren().chongwu
,这个对象名应该符合好记、好认,同时结合第2条
在涉及到一个抽象形容时,尤其适合,比如 Ren.Guanzhu()
,一个人的关注,这个关注按照自然命名法,关注不适宜作为一个对象(暂时理解,后续验证)
按真实世界命名的好处是,在调用时智能提示好理解,有利于编码
5、类之间可以互相嵌套,比如前例:
xxxxxxxxxx
class ren
{
public dongwu chongwu;
public string name;
public ren(string name)
{
this.name = name;
}
}
class dongwu
{
public string name;
public ren zhuren;
public dongwu(ren r,string name)
{
this.name = name;
this.zhuren = r;
r.chongwu = this;
}
}
嵌套之所以可行,是因为自定义类型为引用类型,可以理解为一个指针,一个类甚至可以包含自身类型的字段
xxxxxxxxxx
namespace 类含自己
{
class people
{
public people friend;
}
class Program
{
static void Main(string[] args)
{
people laokou = new people();
laokou.friend = laokou;
}
}
}
之所以类可以包含自身的类型,可以理解为:在创建一个类的实例时,这个类的字段可以为空,所以不会无限循环创建。
当然如果上述代码变成 public people friend=new people()
,编译器不报错,但是很快内存溢出。
上述代码为例,这个类对象的friend
字段也可以是自己
xxxxxxxxxx
class people
{
public people friend;
}
class Program
{
static void Main(string[] args)
{
people laokou = new people();
laokou.friend = laokou;
}
}
这里和真实世界就很相像,一个人的朋友也可以包括自己。编程里可行的原因可以理解为friend
这个字段是个指针指向了对象本身
如果往深里研究,类最终怎么编译成可执行程序,需要去学习编译原理/class
结构,此处不叙。
6、对象的封装、嵌套
A().b.c.d
此种封装很常见,表示A
类型的对象里有一个b
的字段,这个字段是某种类型,其类型里定义了c
的字段,这个字段又是另外一种类型,其定义里有一个d
的字段
xxxxxxxxxx
namespace 多层嵌套
{
class A
{
public B b;
}
class B
{
public C c;
}
class C
{
public void han()
{
Console.WriteLine("我来自于C类型");
}
}
class Program
{
static void Main(string[] args)
{
C c = new C();
B b = new B();
A a = new A();
a.b = b;
b.c = c;
a.b.c.han();//注意这里
}
}
}
类还有很多用法,比如一个类的静态方法返回这个类的一个对象,继而执行其对象方法等,需要不断的学习
7、好玩的用法
xxxxxxxxxx
public static Encoding UTF8
{
get
{
return (Encoding) UTF8Encoding.s_default;
}
}
一个类的静态成员也可以包含着运算,一个类的静态方法返回一个类的对象等等
一个类的某个方法返回这个类型的对象,这种方式很常见(静态方法返回实例,如Task.Run()
返回一个Task类型的对象)
之所以有以上这些总结,是因为个人写的、看的代码很少,所以作为一个小总结使用,成长需要不断的码代码,此文仅供参考
注:1、此文是针对静态语言而写,选择静态语言,我们需要理解关于类型的各种要素。
2、不要把面向对象和此文混肴,python
等动态语言同样具备面行对象特性(之所以写这一条,是个人产生了混肴)
TASK
有一个用法:
var task=Task<int>.Run(()=>{return 5;});
注:按各种教程,前置<>
和后置return
的类型需要相同,但在试验过程中发现后面即使return
一个string
也不报错,所以此处理解暂时存在问题,为了教程,按照Task<T>
和后面Run
方法里的委托返回值同样是T
来考虑,我们尝试其实现方式
using System;
namespace ConsoleApp3
{
public class MyTask<T>//定义一个泛型类
{
public T result;//有一个T类型的属性,名为result
public static MyTask<T> Run(Func<T> t)//把泛型类的T传给func委托、并返回一个此泛型类的实例
{
return new MyTask<T>() { result = t() };//把委托的返回值返给实例的result
}
}
public class People
{
public People son;
public int age;
}
class Program
{
static void Main(string[] args)
{
var task = MyTask<int>.Run(() => { return 3; });
Console.WriteLine(task.result);
var task2 = MyTask<People>.Run(() => { return new People() { age = 15 }; });
Console.WriteLine(task2.result.age);
}
}
}