记录c# 入门级笔记
文章目录
- .Net 生态
- 栈与堆
- 函数指针
- 运算符
- 1. 位移 >> << 左位移 、右位移
- 2. & 、| 、^ 运算符 基于二进制数 做运算
- 属性
- c# 索引器的理解
- 索引器与属性的区别
- 传参方式
- 值类型方式
- 值类型参数
- 引用类型参数
- 引用类型传参方式
- 值类型参数 && 引用类型参数
- 总结 :
- 输出参数
- 数组参数 params
- 函数调用
- 直接调用
- 间接调用
- 委托
- 事件
- 事件模型五个组成部分
- 事件声明完整写法
- 事件声明渐变写法
- 事件和委托的关系及区别
- 面向对象
- 重写
- 插件式设计编程
- 枚举比特位用法
.Net 生态
.Net 发展历程
基础类库源代码
一些实用的资源
- .NET指南– Microsoft针对新手的常规文档;
- .NET Core指南–适用于那些正在考虑过渡到Core版本的用户;
- .NET Framework –.NET的原始文档;
- Xamarin –移动开发文档;
- .NET Foundation– .NET的开源社区;
- NuGet–可查看所有.NET的软件包;
- .NET Core和Framework –提供下载链接。
- .NET开发人员社区–开发人员社区和支持,其中包括:指向社交媒体、论坛、博客、实时研讨会、以及诸如.NET Foundation之类组织的链接。
简单来说,值类型和引用类型变量本身在栈中分配内存,引用类型的实例在堆中分配内存。
- 引用类型变量中存储的值是堆内存上 实例存储地址(内存编号);
- 堆上存储这实例的各个变量数值,并且与栈上的存储数值形式一样
调用方法的时候也会在内存中占有空间
- 方法参数都会存在栈或堆内存中,它的归属者就是 它的调用者。
- 方法内部的局部变量归属于当前方法。
- 方法 是以为某个地址为起点的一段内存中所存储的一组机器语言指令
备注 : cpu寄存器 : cpu中的一块小内存区域; 如果寄存器内存不足 则向内存栈中开辟空间 c# 方法的返回值都存在 寄存器中。
函数指针栈溢出和内存泄漏是 两回事
- 栈溢出是 栈内存不够用了
- 内存泄漏是 堆内存出现了 占用内存的漏洞,对象没有及时被释放的情况
一切皆地址
不论函数与变量都是直接指向的内存地址,所以称为一切皆地址
位移理解可能简单一些:其实就是数据转换成二进制的左右移动;右移左补0,左移右补0,后面多出来的部分去掉。
用乘除法去理解位移也可以:
左位移:相当于乘
左移1位相当于乘2,左移2位相当于乘4,左移3位相当于乘8,左移4位相当于乘16…类推
右位移:相当于除
右移1位相当于除2,右移2位相当于除4,右移3位相当于除8,右移4位相当于除16…类推
属性是对字段的进化、也可以理解为封装。 目的是为了对字段起到保护、和扩展字段逻辑的措施。
c# 索引器的理解通常情况下,属性只能访问单一的字段(一个属性对一个字段进行封装),如果想访问多个数据成员,就需要使用索引器。索引器是类的特殊成员,它可以根据索引在多个成员中进行选择,能够让对象以类似数组的方式来存取。索引器的数据类型必须是统一的。
备注 : 索引器与属性的概念很类似 get;set; 用法一致
索引器参数概览
- 使用索引器可以用类似于数组的方式为对象建立索引。
- get 取值函数返回值。 set 取值函数分配值。
- this 关键字用于定义索引器。
- value 关键字用于定义由 访问器分配的值。 (value参数类型与定义索引器的数据类型是一致的)
- 索引器不必根据整数值进行索引;由你决定如何定义特定的查找机制。
- 索引器可被重载。
- 索引器可以有多个形参,例如当访问二维数组时。
每次传参方法内部都会创建一个参数的副本 也就是创建出来一个新的副本(占用新的内存空间)
备注: 由于值类型存储在栈内存上的,并且值类型变量所对应的value就是变量值。 所以会出现 方法内的值类型变量更新不影响外部值类型参数。
每次传参方法内部也会创建一个参数的副本 也会跟值类型的方式一样。
但是 由于引用类型变量 是在栈上存储着一个堆内存上的地址, 所以会出现 两个引用类型变量虽然 是两个不同的变量但他们的值都指向的同一个堆内存地址。
他们传参的时候 内部不会再创建新的参数副本了, 会直接引用实参的内存地址进行传参。
总结 :ref 关键字在本质在于传递的内存地址的不同。
值类型方式传递 是会创建新的内存地址
引用类型方式传递不会创建,而是直接引用实参的内存地址
虽然结果一样 咱底层的内存机理是不一样的。
场景: 在方法返回值无法满足你的需求时, 输出参数也可以返回数据。
输出参数与ref类似 在输出参数时,不会在内存中创建副本且使用前不用为输出参数赋值。
数组可以作为实参传递给方法形参。 由于数组是引用类型,因此方法可以更改元素的值。
函数调用一切皆地址
1. 变量(数据) 是以某一地址为起点的一段内存中所存储的值
2. 函数(算法) 是以某一地址为起点的一段内存中所存储的一组机器语言指令
函数分为直接调用和间接调用
直接调用通过函数名来调用函数, cpu通过函数名直接获取函数所在地址并开始执行
间接调用通过函数指针来调用函数, cpu通过函数指针存储的值获取函数所在地址并开始执行
委托事件 事件模型五个组成部分 事件声明完整写法委托是一种类 , 类是数据类型所以委托也是一种数据类型; 他的声明与类不同,语法参照 c/c++ 函数指针的语法
委托是 c/c++语言函数指针的升级版
委托数据类型
///事件声明渐变写法/// 声明委托类型 点菜委托 /// /// 事件拥有者 /// 委托消息参数 public delegate void OrderEventHandler(Customer customer, OrderEventArgs e); public class Customer { ////// 这部分就是 事件所谓的 事件是基于委托的 (事件的完整声明) /// private OrderEventHandler orderEventHandler; public event OrderEventHandler Order { add { this.orderEventHandler += value; } remove { this.orderEventHandler -= value; } } }
// 声明事件的简化方式 (容易理解成事件是字段的错觉)
public event OrderEventHandler Order;
事件和委托的关系及区别
- 事件是基于委托的 但并不是一个特殊的委托, 基于委托做类型约束的;
- 当事件的响应者向事件的拥有者提供了事件处理器之后 能够将事件处理器能够存储起来 (此处只有委托可以实现, 因为委托可以引用方法)
- 因为委托是一个可以新增、移除、调用的方法成员 可操作性高; 而事件则是委托的封装后的成员类型(包装器),外部只允许 += 、 -= 操作符 事件拥有者内部可以直接使用到该事件的委托进行操作。 进一步提高了 委托的安全性。(由于c# 有个语法糖 让我们在内部可以用委托的方式来使用事件,其实是个语法糖,编译器会生成该委托)
// 事件原则上只可以使用 "+= , -=" 操作符的。
// 事件拥有者内部可以通过事件使用其他操作符的原因是因为
// 此处有个语法糖 编译器会把事件编译为 委托数据类型让你使用。 所以才会出现以下现象
public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
public class Customer
{
// 声明事件的简化方式 (容易理解成事件是字段的错觉)
public event OrderEventHandler Simplify_Order;
// 思考 点菜的动作执行
public void Think()
{
if (this.Simplify_Order != null)
{
OrderEventArgs orderEventArgs = new OrderEventArgs();
orderEventArgs.DishName = "宫保鸡丁";
orderEventArgs.Size = "small";
// 触发事件
this.Simplify_Order.Invoke(this, orderEventArgs);
}
}
}
例如:window form 事件组成模式
事件拥有者属于事件响应者的一个成员字段。 并在事件响应者中为事件拥有者成员字段 订阅事件处理器。
自定义事件实例:
using System;
using System.Threading.Tasks;
using System.Threading;
namespace 自定义事件
{
class Program
{
static void Main(string[] args)
{
// 普通事件组成模式
Customer customer = new Customer();
Watier watier = new Watier();
customer.Order += new OrderEventHandler(watier.Action);
// 事件触发 是事件拥有者内部的逻辑来触发
customer.Action();
customer.PayTheBill();
}
}
// 示例场景 : 顾客点菜, 服务员下单的一个场景 事件触发逻辑 顾客点菜后服务员下单的事件
///
/// 声明委托类型 点菜委托
///
/// 事件拥有者
/// 委托消息参数
public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
///
/// 事件消息参数 主要用于传递所需的事件参数
///
public class OrderEventArgs : EventArgs
{
///
/// 菜名
///
public string DishName { get; set; }
///
/// 菜量
///
public string Size { get; set; }
}
///
/// 顾客 动作: 账单支付、 点菜的事件
///
public class Customer
{
///
/// 这部分就是 事件所谓的 事件是基于委托的 (事件的完整声明)
///
private OrderEventHandler orderEventHandler;
public event OrderEventHandler Order
{
add {
this.orderEventHandler += value;
}
remove
{
this.orderEventHandler -= value;
}
}
// 声明事件的简化方式 (容易理解成事件是字段的错觉)
public event OrderEventHandler Simplify_Order;
///
/// 账单价格
///
public double Bill { get; set; }
public void PayTheBill()
{
// 我会支付这盘菜
Console.WriteLine("I whill pay the dish ${0}", this.Bill);
}
public void Walkln()
{
// 走进餐厅
Console.WriteLine("Walk into the restaurant");
}
public void Sitdown()
{
Console.WriteLine("坐下");
}
// 思考 点菜的动作执行
public void Think()
{
for (int i = 0; i < 5; i++)
{
//想想
Console.WriteLine("Let me think.......");
Thread.Sleep(1000);
}
// 判断是否 有服务生订阅了事件
if (this.orderEventHandler !=null)
{
OrderEventArgs orderEventArgs = new OrderEventArgs();
orderEventArgs.DishName = "鱼香肉丝";
orderEventArgs.Size = "small";
this.orderEventHandler.Invoke(this, orderEventArgs);
}
if (this.Simplify_Order != null)
{
OrderEventArgs orderEventArgs = new OrderEventArgs();
orderEventArgs.DishName = "宫保鸡丁";
orderEventArgs.Size = "small";
this.Simplify_Order.Invoke(this, orderEventArgs);
}
}
public void Action()
{
this.Walkln();
this.Sitdown();
this.Think();
}
}
///
/// 服务生
///
public class Watier
{
///
/// 事件处理函数
///
///
///
public void Action(Customer customer, OrderEventArgs e)
{
Console.WriteLine("I will serve you the dish -{0}", e.DishName);
double price = 10;
switch (e.Size)
{
case "small":
price = price * 0.5;
break;
case "large":
price = price * 1.5;
break;
default:
break;
}
//总账单
customer.Bill += price;
}
}
}
类三大核心成员
- 属性: 存储数据
- 方法: 做事情
- 事件: 通知其他对象
overrid 和 hide 方法的区别
overrid 会在出现is a 概念的情况下一直沿着继承链从高到低(从base开始) 找到最新的子类的方法,当中如果出现子类有hide影响则中断从继承链的查找了,就断在Car类这个Run方法了,最后会执行 Car的Run函数
- 编程中 把确定的不易改变的成员封装起来, 把易改变的成员抽象起来为抽象成员
如果不是需要修改BUG或添加新功能就不应该随意更改代码
抽象类就符合 开放/关闭原则
接口则是面向对象中最抽象化的实现了, 好处: 极大的抽象化、 加大了代码的 “高内聚低耦合”
耦合关系图
依赖倒置的理解:
假设场景: 司机开车。
一般依赖关系: 司机开车需要依赖car车 卡车开车需要依赖trucker卡车 这是属于一般的逻辑。
使用接口后的依赖关系: 司机抽象依赖于IVehicle交通工具的接口, 而car汽车、trucker卡车依赖于IVehicle交通工具并实现了交通工具的抽象。 形成了我们一般逻辑上的反转。 让司机依赖于抽象、不同车型依赖于交通工具的抽象。
两者的区别: 第一种 具体实现依赖于具体实现 = 汽车司机依赖于汽车、 卡车司机依赖于卡车; 而第二种则是抽象依赖于抽象 司机抽象起来、交通工具抽象起来, 司机抽象依赖于交通工具的抽象最终实现 最大程度上的低耦合关系 也符合面向对象 设计五大原则
接口显式实现
只有用接口类型变量来引用实例时才可调用此接口函数。其他类型无法调用
- 主程序如果希望自身的功能可以被扩展,其需要:
1.1 提供一组服务 (Service Interface)。其提供(约束)了插件对主体能力可控制的边界。服务定义的越精细,插件控制的粒度越小,能力越大,但耦合度和复杂度也越高。
1.2 定义一种扩展契约 (Plug-In Interface),其描述了插件应该如何设计以便于主程序发现。并通过插件管理模块 (Plug-In Manager) 来发现、维护插件。 - 插件通过实现主程序规定的扩展契约(通常是一个接口),标明自己的身份,并接收来自主程序的事件响应。通过调动主程序提供的服务,实现和主程序的交互。这一过程,通常都是被主程序以 SDK (Software Development Kit) 的形式封装。基本上我们日常所有能接触到的应用都或多或少的提供了插件机制,比如操作系统。其上面运行的应用程序即可以理解为它的插件实现。
反射实现松耦合的最大化则是 插件式编程
插件式编程的好处: 是主体程序为中心, 把插件用于主题程序结合使用。 做到 以不变应万变的情况。
实现架构图
插件机制原理
常用插件式架构模式
枚举比特位用法 enum Level
{
Employe,
Manger,
Boos,
BigBoss
}
// 枚举赋多个值
Level res = Level.BigBoss | Level.BigBoss | Level.Manger;
// 判断是否有这个等级
if ((res & Level.Boos)== Level.BigBoss)
{
}
此处用了 | & 两个二进制操作符



