InfoPool

私人信息记录

0%

我们使用Angular Guards来控制用户是否可以导航到或离开当前路由。

我们使用路由守卫的常见场景之一是身份验证。我们希望我们的应用程序能够阻止未经授权的用户访问受保护的路由。当用户试图导航到受保护的路由时,angular调用CanActivate。然后,我们挂接到CanActivate防护程序,并使用身份验证服务来检查用户是否有权使用路由,如果没有,我们可以将用户重定向到登录页面。

守卫路由的作用

  • 确认导航操作
  • 询问是否在离开视图之前进行数据保存
  • 允许特定用户访问应用程序的某些部分
  • 在导航到新路由之前验证路由参数
  • 在显示组件之前获取一些数据

路由守卫类型

Angular Router支持五种不同的路由守卫,您可以使用它们来保护路由

  • CanActivate:决定是否可以激活路由(或使用组件)。在用户未被授权导航到目标组件的情况下,此保护非常有用。或者用户可能没有登录到系统
  • CanDeactivate:决定用户是否可以离开组件(导航离开当前组件)。在用户可能有一些未保存的挂起更改的情况下,此路由非常有用。
  • Resolve:延迟路由的激活,直到某些任务完成。在激活路由之前,您可以使用Resolve守卫从后端API获取数据
  • CanLoad:防止加载延迟加载模块。我们通常在不希望未经授权的用户看到模块的源代码时使用此保护。CanLoad与CanActivate类似,但有一点不同。CanActivate防止访问特定的路由。CanLoad防止整个延迟加载模块被下载,从而保护该模块中的所有路由。
  • CanActivateChild:确定是否可以激活子路由。CanActivateChild与CanActivate非常相似。我们将CanActivateChild应用于父路由。每当用户试图导航到其任何子路由时,Angular都会调用CanActivateChild。这使我们能够检查某些情况,并决定是继续导航还是取消导航。

如何创建Angular路由守卫

  • 建立Guard服务。

    @Injectable()
    export class ProductGuardService implements CanActivate {}

  • 在服务中实现Guard方法

    canActivate(): boolean {
    // Check weather the route can be activated;
    return true;
    // or false if you want to cancel the navigation;
    }

  • 在根模块中注册Guard服务

    providers: [ProductService,ProductGuardService]

  • 更新路由以使用Guard服务

    { path: ‘product’, component: ProductComponent, canActivate : [ProductGuardService] }

守卫路由调用顺序

添加路由守卫语法

1
2
3
4
5
6
7
{ path: 'product', component,        
canActivate : any[],
canActivateChild: any[],
canDeactivate: any[],
canLoad: any[],
resolve: any[]
}

一条路由可以有多个守卫保护,并且可以在路由层次结构的每个级别都有守卫保护。

  • 始终首先检查CanDeactivate()和CanActivateChild()防护。检查从最深的子路径开始到顶部。
  • 接下来将检查CanActivate()保护,并从顶部开始检查最深的子路由。
  • 接下来调用CanLoad(),如果要异步加载功能模块。
  • Resolve()Guard是最后调用的。

如果任何防护返回false,则Angular路由器将取消导航。

什么是路由

路由允许您从应用程序的一部分移动到另一部分,或者从一个视图移动到另一个视图。

在Angular中,路由由Angular路由器模块处理。

路由器是Angular中的一个独立模块。它在@angular/router包中。Angular Router提供了应用程序视图中导航所需的服务和指令。

使用Angular Router,您可以

  • 通过在地址栏中键入URL导航到特定视图
  • 将可选参数(查询参数)传递到视图
  • 将可单击元素绑定到视图,并在用户执行应用程序任务时加载视图
  • 处理浏览器的后退和前进按钮
  • 允许您动态加载视图
  • 使用路由保护程序保护路由不受未经授权的用户的攻击

Angular路由组件

Router

Angular Router 是服务(service)(Angular Router API),它允许用户在执行应用程序任务时从一个组件导航到下一个组件,如单击菜单链接和按钮,或单击浏览器上的后退/前进按钮。我们可以访问router对象,并使用其方法(如navigate()或navigateByUrl())来导航到路由.

Route

当用户单击链接或将URL粘贴到浏览器地址栏时,Route 告诉Angular Router要显示哪个视图。每个Route都由一个路径和映射到的组件组成。Router对象使用Route解析并构建最终的URL.

图 0

Routes

Routes是应用程序支持的Route对象的数组

RouterOutlet

RouterOutlet是一个用作占位符的指令(<router-outlet>),路由器应在其中显示视图

选择器

router-outlet

模板变量参考手册

outlet #myTemplateVar=”outlet”

每当新组件实例化之后,路由出口就会发出一个激活事件;在销毁时则发出取消激活的事件。

1
2
3
4
5
<router-outlet
(activate)='onActivate($event)'
(deactivate)='onDeactivate($event)'
(attach)='onAttach($event)'
(detach)='onDetach($event)'></router-outlet>

每个出口可以具有唯一的名称,该 name 由可选的 name 属性确定。该名称不能动态设置或更改。如果未设置,则默认值为 “primary”。

1
2
3
<router-outlet></router-outlet>
<router-outlet name='left'></router-outlet>
<router-outlet name='right'></router-outlet>
1
{path: <base-path>, component: <component>, outlet: <target_outlet_name>}

使用命名的出口和辅助路由,你可以在同一 RouterLink 指令中定位多个出口。

路由器在导航树中跟踪每个命名出口的单独分支,并在 URL 中生成该树的表示形式。辅助路由的 URL 使用以下语法同时指定主要路由和辅助路由:

1
http://base-path/primary-route-path(outlet-name:route-path)

RouterLink是一个将HTML元素绑定到Route的指令。单击绑定到RouterLink的HTML元素,将导航到Route。RouterLink可能包含要传递给路由组件的参数。

给定路由配置

1
[{ path: 'user/:name', component: UserCmp }]

,以下内容将创建一个到该路由的静态链接:

1
<a routerLink="/user/bob">link to user component</a>

你也可以使用动态值来生成链接。对于动态链接,请传递路径段数组,然后传递每个段的参数。比如,
['/team', teamId, 'user', userName, {details: true}] 生成到 /team/11/user/bob;details=true

多个静态段可以合并为一个词,并与动态段组合。比如,['/team/11/user', userName, {details: true}]

你提供给链接的输入将被视为当前 URL 的增量。比如,假设当前 URL 是 /user/(box//aux:team)。则链接 <a [routerLink]="['/user/jim']">Jim</a> 会创建 URL /user/(jim//aux:team)

你可以在链接中使用绝对或相对路径、设置查询参数、控制如何处理参数以及保留导航状态的历史记录。

RouterLinkActive

RouterLinkActive是用于从绑定到RouterLink的HTML元素中添加或删除CSS样式的指令。使用此指令,我们可以根据当前RouterState切换RouterLinks的CSS样式

ActivatedRoute

ActivatedRoute是一个对象,表示与加载的组件关联的当前激活的路由。

RouterState

如何使用 Angular路由

以下几步来配置使用路由

  • 设置
  • 定义视图的管线
  • 向Routes注册路由器服务
  • 将HTML元素操作映射到路由
  • 选择要显示视图的位置

路由策略

在Angular中支持两种不同的路由策略。一个是PathlocationStrategy,另一个是HashLocationStrategy。HashLocationStrategy使用Hash风格的路由,而PathlocationStrategy使用HTML5路由。

我们建议使用HTML5样式(PathLocationStrategy)作为定位策略。因为

  • 它产生干净和SEO友好的URL,用户更容易理解和记住。
  • 可以利用服务器端渲染,这将使我们的应用程序加载更快,方法是在将页面交付给客户端之前先在服务器中渲染页面

只有当软件必须支持较旧的浏览器时,才使用哈希位置策略。

路由参数

定义路由

我们可以通过添加’/:’和一个占位符(id)来定义参数,如下所示

{ path: 'product/:id', component: ProductDetailComponent }

其中id是路由的动态部分。
现在,上面的路径与URL /product/1、/product/2等相匹配。

如果您有多个参数,那么您可以通过继续添加占位符来扩展它

{ path: 'product/:id/:id1/:id2', component: ProductDetailComponent }

名称id、id1和id2是参数的占位符。我们将在检索参数值时使用它们。

定义导航

我们现在需要提供带有路径和路由参数的routerLink指令。通过将产品的id作为第二个元素添加到routerLink参数数组中来完成,如下所示

<a [routerLink]="['/Product', ‘2’]">{{product.name}} </a>

编译为URL /product/2

<a [routerLink]="['/Product', product.productID]">{{product.name}} </a>

动态地从产品对象中获取id的值。

您也可以使用路由器对象的导航方法

1
2
3
4
5
goProduct() {     
this.router.navigate(
['/products'. product.productID] }
);
}

获取路由参数

ActviatedRoute

ActivatedRoute是一项服务,用于跟踪与加载的组件相关联的当前激活的路由。

constructor(private _Activatedroute:ActivatedRoute)

ActviatedRoute路由提供了两个属性含Route参数。

  • ParamMap
  • Params

ParamMap

Angular将所有路由参数的映射添加到ParamMap对象中,该对象可以从ActivatedRoute服务访问

ParamMap有三种方法,可以更容易地使用参数。

  • get方法检索给定参数的值。
  • getAll方法检索所有参数
  • has 如果ParamMap包含给定的参数,则has方法返回true,否则返回false

Params

ActviatedRoute还维护“参数数组“Params。Params数组是一个参数值列表,按名称进行索引。

读取参数

使用ActivatedRoute有两种方法从ParamMap对象获取参数值。

  • 使用ActivatedRoute的Snapshot属性
  • 订阅ActivatedRoute的paramMap或params observable属性

使用 Snapshot

1
2
3
4
this.id=this._Activatedroute.snapshot.paramMap.get("id");

this.id=this._Activatedroute.snapshot.params["id"];

使用 Observable

1
2
3
4
5
6
7
this._Activatedroute.paramMap.subscribe(paramMap => { 
this.id = paramMap.get('id');
});

this._Activatedroute.params.subscribe(params => {
this.id = params['id'];
});

ActivatedRoute

ActivatedRoute服务提供了大量有用的信息,包括:

url:此属性返回url Segment对象的数组,每个对象描述url中与当前路由匹配的单个段。
params:此属性返回一个params对象,该对象描述按名称索引的URL参数。
queryParams:此属性返回一个Params对象,该对象描述按名称索引的URL查询参数。
fragment:此属性返回一个包含URL片段的字符串。
snapshot:此路由的初始快照
data:包含为路线提供的数据对象的Observable
component:路由的组件。这是一个常数
outlet:用于渲染路由的RouterOutlet的名称。对于未命名的出口,为primary。
routeConfig:用于包含原始路径的路由的路由配置。
parent:ActivatedRoute,包含使用子路由时来自父路由的信息。
firstChild:包含子路由列表中的第一个ActivatedRoute。
children:包含在当前路线下激活的所有子路线
pathFromRoot:从路由器状态树的根到该路由的路径

路由Query参数

Query参数是在 URL中的 ?右侧的键值对,多个Query参数之间由&分隔。

/product?page=2&filter=all

在上面的例子中,page=2和filter=all是Query参数。它包含两个Query参数。一个是Page,其值为2,另一个是Filter,其值是all。

添加Query参数

Query参数不是路由的一部分。因此,您不会像路由参数那样在 routes 中定义它们。有两种方法可以将Query参数传递给路由

  • 使用routerlink指令
  • 使用router.navigation方法。
  • 使用router.navigateByUrl方法

我们使用routerlink指令的queryParams属性来添加Query参数。我们将此指令添加到模板文件中。

<a [routerLink]="['product']" [queryParams]="{ page:2 }">Page 2</a>

路由器将URL构造为

/product?page=2

您可以传递多个Query参数,如下所示

<a [routerLink]="['products']" [queryParams]="{ color:'blue' , sort:'name'}">Products</a>

路由器将URL构造为

/products?color=blue&sort=name

在组件中使用 router.navigate

1
2
3
4
5
6
7
goTo() {     
this.router.navigate(
['/products'],
{ queryParams: { page: 2, sort:'name'} }
);
}

路由器将URL构造为

/products?page=2&sort=name

在组件中使用 router.navigateByUrl

1
this.router.navigateByUrl('product?pageNum=2');

读取 路由Query参数

读取Query参数类似于读取Router参数。有两种方法可以检索查询参数。

  • 订阅queryParamMap或queryParams observable
  • 使用snapshot属性的queryParamMap或queryParams属性

以上两项都是ActivatedRoute服务的一部分。因此,我们需要将它注入到我们的组件类中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
this.sub = this.Activatedroute.queryParamMap
.subscribe(params => {
this.pageNum = +params.get('pageNum')||0;
});

this.sub = this.Activatedroute.queryParams
.subscribe(params => {
this.pageNum = +params.['pageNum']||0;
});

this.Activatedroute.snapshot
.queryParamMap.get('pageNum')||0;;

this.Activatedroute.snapshot
.queryParams['pageNum']||0;;

网格容器

我们通过在元素上声明 display:grid 或 display:inline-grid 来创建一个网格容器。一旦我们这样做,这个元素的所有直系子元素将成为网格元素。

网格轨道

我们通过 grid-template-columns 和 grid-template-rows 属性来定义网格中的行和列。这些属性定义了网格的轨道。一个网格轨道就是网格中任意两条线之间的空间。

fr 单位

轨道可以使用任何长度单位进行定义。网格还引入了一个另外的长度单位来帮助我们创建灵活的网格轨道。新的fr单位代表网格容器中可用空间的一等份。下一个网格定义将创建三个相等宽度的轨道,这些轨道会随着可用空间增长和收缩。

网格线

应该注意的是,当我们定义网格时,我们定义的是网格轨道,而不是网格线。Grid 会为我们创建编号的网格线来让我们来定位每一个网格元素。

网格线的编号顺序取决于文章的书写模式。在从左至右书写的语言中,编号为 1 的网格线位于最左边。在从右至左书写的语言中,编号为 1 的网格线位于最右边。网格线也可以被命名,我们将在稍后的教程中看到如何完成这一操作。

网格间距

在两个网格单元之间的 网格横向间距 或 网格纵向间距 可使用 grid-column-gap 和 grid-row-gap 属性来创建,或者直接使用两个合并的缩写形式 grid-gap。间距使用的空间会在 使用弹性长度 fr 的轨道的空间计算前就被留出来,间距的尺寸定义行为和普通轨道一致,但不同的是你不能向其中插入任何内容。

使用 z-index 控制层级

多个网格项目可以占用同一个网格单位。如果我们回到之前根据网格线编号放置网格项目的话,我们可以更改此项来使两个网格项目重叠,其覆盖顺序遵循文档流的原始顺序(后来居上)。

我们可以在网格项目发生重叠时使用 z-index 属性控制重叠的顺序——就像放置网格项目一样。如果我们给 box2 设定一个低于 box1 的 z-index 值的话,box2 将会显示在 box1 的下方。

我们将探索应用于弹性(flex)元素的三个属性,它们能使我们在主轴方向上控制弹性元素的尺寸和伸缩性——flex-grow、flex-shrink 和 flex-basis。

这三个属性控制弹性元素的以下几个方面的灵活性:

  • flex-grow:该元素(拉伸)获得多少正可用空间
  • flex-shrink:该元素(收缩)要消除多少负可用空间
  • flex-basis:在该元素未拉伸和收缩之前,它所占空间

在考虑 flex 属性如何在主轴方向上控制比率之前,有一些概念值得我们去深究。这涉及到弹性元素在任何伸缩之前的自然尺寸,以及可用空间(free space)的概念。

弹性元素的尺寸

在 CSS 中还有 min-content 和 max-content 这两个概念;这两个关键字可以用来代替长度单位。

  • min-content:本质上讲,就是字符串中最长的单词长度决定的大小。
  • max-content:代表了内容的最大宽度或最大高度。对于文本内容而言,这意味着内容即便溢出也不会被换行。

正负可用空间

当一个弹性容器有正可用空间时,它就有更多的空间用于在容器内显示弹性元素。比如说,如果我们有 500px 宽的容器,flex-direction 属性值为 row,三个 100px 宽的弹性元素,那么我们还有 200px 的正可用空间,如果我们想要填充整个容器,则可将其分配到元素中。

图 0

当弹性元素的自然尺寸加起来比弹性容器内的可用空间大时,我们产生了负可用空间。比如我们有一个像上面那样的 500px 宽的容器,但是三个弹性元素每个都为 200px 宽,那我们就一共需要 600px 宽,因此就有了 100px 的负可用空间。这可以从弹性元素中删除以使其能适应容器。

图 1

flex-basis 属性

flex-basis 属性在任何空间分配发生之前初始化弹性元素的尺寸。此属性的初始值为 auto。如果 flex-basis 设置为 auto,浏览器会先检查元素的主尺寸是否设置了绝对值再计算出它们的初始值。比如说你已经给你的元素设置了 200px 的宽,则 200px 就是这个元素的 flex-basis。

如果你的元素为自动调整大小,则 auto 会解析为其内容的大小。此时你所熟知的 min-content 和 max-content 大小会变得有用,弹性盒子会将元素的 max-content 大小作为 flex-basis。

空间分配时,如果你想要弹性盒子完全忽略元素的尺寸就需要设置 flex-basis 为 0。这显式地说明弹性盒子可用抢占所有空间,并按比例进行分配。

flex-basis属性会覆盖项目的width属性(当主轴方向水平时)或height属性(当主轴方向垂直时),然而,flex-basis属性仍然受max-width/max-height和min-width/min-height属性的约束。

flex-grow 属性

flex-grow 属性指定了弹性增长因子(flex grow factor),这决定了在分配正可用空间时,弹性元素相对于弹性容器中的其余弹性元素的增长程度。

如果 flex-grow 的值全部相同,并且在弹性容器中还有正可用空间,那么它就会被平均地分配给所有元素。

flex-shrink 属性

flex-shrink 属性指定了弹性收缩因子(flex shrink factor),它确定在分配负可用空间时,弹性元素相对于弹性容器中其余弹性元素收缩的程度。

掌握弹性元素的大小

什么设置了元素的基本大小?

  1. flex-basis 设置为 auto,且元素设置了宽度,元素的大小将会基于设置的宽度。
  2. flex-basis 设置为 auto 或 content(在支持的浏览器中),元素的大小为原始大小。
  3. flex-basis 设置为 非0的长度,那这就是元素的大小。
  4. flex-basis 设为了 0,则元素的大小不在空间分配计算的考虑之内。

我们有可用空间吗?

  1. 元素没有正可用空间就不会增长,没有负可用空间就不会缩小。
  2. 如果我们把所有元素的宽度相加(如果在列方向工作则为高度),总和小于容器的总宽度(或高度),那么你有正可用空间,并且 flex-grow 会发挥作用。
  3. 如果我们把所有的元素的宽度相加(如果在列方向工作则为高度),总和大于容器的总宽度(或高度),那么你有负可用空间,并且 flex-shrink 会发挥作用。

关于flex-grow属性的计算过程为

  1. flex-grow属性小于等于0的项目不受影响,不会伸长。
  2. 对剩余项目的flex-grow属性进行求和,将结果记为sum。
  3. 分以下情况进行讨论。

sum ≥ 1 时 计算过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

.container {
display: flex;
width: 800px;
.item1 {
flex-basis: 50px;
}
.item2 {
flex-basis: 100px;
flex-grow: 1;
}
.item3 {
flex-basis: 150px;
flex-grow: 3;
}
.item4 {
flex-basis: 200px;
flex-grow: 6;
}
}
  1. 计算剩余宽度:剩余宽度 = 容器宽度 - 项目总宽度,所以:剩余宽度 = 800px - 500px = 300px。
  2. 计算各项目权重占比:权重 = ,所以:
    1. 总权重 = sum = 1 + 3 + 6 = 10。
    2. 项目2权重 = 1,项目2权重占比 = 1 / 10 = 0.1。
    3. 项目3权重 = 3,项目3权重占比 = 3 / 10 = 0.3。
    4. 项目4权重 = 6,项目4权重占比 = 6 / 10 = 0.6。
  3. 计算各项目伸长宽度:伸长宽度 = 权重占比 * 剩余宽度,所以:
    1. 项目2伸长宽度 = 0.1 * 300px = 30px。
    2. 项目3伸长宽度 = 0.3 * 300px = 90px。
    3. 项目4伸长宽度 = 0.6 * 300px = 180px。
  4. 计算各项目伸长后宽度:伸长后宽度 = + 伸长宽度,所以:
    1. 项目2伸长后宽度 = 100px + 30px = 130px。
    2. 项目3伸长后宽度 = 150px + 90px = 240px。
    3. 项目4伸长后宽度 = 200px + 180px = 380px。

sum < 1 时

计算过程基本与sum > 1时的情况相同,但在第 3 步中,各项目的伸长宽度为sum * 权重占比 * 剩余宽度。

关于flex-shrink属性的计算过程为

  1. flex-shrink属性小于等于0的项目不受影响,不会缩短。
  2. 对剩余项目的flex-shrink属性进行求和,将结果记为sum。
  3. 分以下情况进行讨论。

sum ≥ 1 时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.container {
display: flex;
width: 800px;
.item1 {
flex-basis: 100px;
}
.item2 {
flex-basis: 200px;
flex-shrink: 6;
}
.item3 {
flex-basis: 300px;
flex-shrink: 3;
}
.item4 {
flex-basis: 400px;
flex-shrink: 1;
}
}
  1. 计算溢出宽度:溢出宽度 = 项目总宽度 - 容器宽度,所以:溢出宽度 = 800px - 1000px = 200px。
  2. 计算各项目权重占比:权重 = * ,所以:
    1. 总权重 = 1200 + 900 + 400 = 2500。
    2. 项目2权重 = 1200,项目2权重占比 = 1200 / 2500 = 0.48。
    3. 项目3权重 = 900,项目3权重占比 = 900 / 2500 = 0.36。
    4. 项目4权重 = 400,项目4权重占比 = 400 / 2500 = 0.16。
  3. 计算各项目缩短宽度:缩短宽度 = 权重占比 * 溢出宽度,所以:
    1. 项目2伸长宽度 = 0.48 * 200px = 96px。
    2. 项目3伸长宽度 = 0.36 * 200px = 72px。
    3. 项目4伸长宽度 = 0.16 * 200px = 32px。
  4. 计算各项目缩短后宽度:缩短后宽度 = 原始宽度 - 缩短宽度,所以:
    1. 项目2缩短后宽度 = 200px - 96px = 104px。
    2. 项目3缩短后宽度 = 300px - 72px = 228px。
    3. 项目4缩短后宽度 = 400px - 32px = 368px。

sum < 1 时

计算过程基本与sum > 1时的情况相同,但在第 3 步中,各项目的缩短宽度为sum * 权重占比 * 溢出宽度。

传统的布局方案,基于盒模型,依赖display属性、position属性和float属性。其对于某些特殊的布局(如垂直居中),实现起来比较麻烦。
在 2009 年,W3C 提出了一种新的布局方案——Flex 布局(Flexible Box 布局,弹性盒子布局),其相比于传统的布局方案,更为灵活和简便。目前,Flex 布局已经得到了所有浏览器的支持。
将元素的display属性设置为flex或inline-flex后,即可开启 Flex 布局:

1
2
3
4
5
6
7
8
9
// 块级元素
.block-box {
display: flex;
}

// 行内元素
.inline-box {
display: inline-flex;
}

同时,该元素会自动成为 Flex 容器,简称容器。且容器的所有子元素会自动成为 Flex 容器成员,简称项目。项目的float、clear、vertical-align属性将失效。
容器中默认存在两根轴:主轴和交叉轴。主轴和交叉轴互相垂直,类似于平面坐标系中的 x 轴和 y 轴。项目将自动沿着主轴方向排列,排满时将沿交叉轴方向堆砌,即在交叉轴方向上换行(前提是容器flex-wrap属性不为nowrap)。

flexbox 的两根轴线

当使用 flex 布局时,首先想到的是两根轴线 — 主轴和交叉轴。主轴由 flex-direction 定义,另一根轴垂直于它。我们使用 flexbox 的所有属性都跟这两根轴线有关,所以有必要在一开始首先理解它。

主轴

主轴由 flex-direction 定义,可以取 4 个值:

  • row
  • row-reverse
  • column
  • column-reverse

如果你选择了 row 或者 row-reverse,你的主轴将沿着 inline 方向延伸。
图 0

选择 column 或者 column-reverse 时,你的主轴会沿着上下方向延伸 — 也就是 block 排列的方向。

图 1

交叉轴

交叉轴垂直于主轴,所以如果你的flex-direction (主轴) 设成了 row 或者 row-reverse 的话,交叉轴的方向就是沿着列向下的。

图 2

如果主轴方向设成了 column 或者 column-reverse,交叉轴就是水平方向。
图 3

理解主轴和交叉轴的概念对于对齐 flexbox 里面的元素是很重要的;flexbox 的特性是沿着主轴或者交叉轴对齐之中的元素。

起始线和终止线

过去,CSS 的书写模式主要被认为是水平的,从左到右的。现代的布局方式涵盖了书写模式的范围,所以我们不再假设一行文字是从文档的左上角开始向右书写,新的行也不是必须出现在另一行的下面。

如果 flex-direction 是 row ,并且我是在书写英文,那么主轴的起始线是左边,终止线是右边。
图 4
如果我在书写阿拉伯文,那么主轴的起始线是右边,终止线是左边。

在这两种情况下,交叉轴的起始线是 flex 容器的顶部,终止线是底部,因为两种语言都是水平书写模式。

Flex 容器

文档中采用了flexbox的区域就叫做flex容器。为了创建 flex容器,我们把一个容器的display属性值改为flex 或者inline-flex。完成这一步之后,容器中的直系子元素就会变为flex元素。所有 CSS 属性都会有一个初始值,所以 flex 容器中的所有 flex 元素都会有下列行为:

  • 元素排列为一行 (flex-direction 属性的初始值是 row)。
  • 元素从主轴的起始线开始。
  • 元素不会在主维度方向拉伸,但是可以缩小。
  • 元素被拉伸来填充交叉轴大小。
  • flex-basis 属性为 auto。
  • flex-wrap 属性为 nowrap。

这会让你的元素呈线形排列,并且把自己的大小作为主轴上的大小。如果有太多元素超出容器,它们会溢出而不会换行。如果一些元素比其他元素高,那么元素会沿交叉轴被拉伸来填满它的大小。

更改 flex 方向 flex-direction

设置 flex-direction: row-reverse 可以让元素沿着行的方向显示,但是起始线和终止线位置会交换。

把 flex 容器的属性 flex-direction 改为 column ,主轴和交叉轴交换,元素沿着列的方向排列显示。改为 column-reverse ,起始线和终止线交换。

flex-direction 是 row,意味着主轴是水平方向,交叉轴对齐则是垂直方向由上至下。

图 3

flex-direction 为 column ,意味着主轴是垂直方向,交叉轴对齐则是水平方向由左至右。

图 4

用 flex-wrap 实现多行 Flex 容器

虽然flexbox是一维模型,但可以使我们的flex项目应用到多行中。在这样做的时候,您应该把每一行看作一个新的flex容器。任何空间分布都将在该行上发生,而不影响该空间分布的其他行。

为了实现多行效果,请为属性flex-wrap添加一个属性值wrap。现在,如果您的项目太大而无法全部显示在一行中,则会换行显示。下面的实时例子包含已给出宽度的项目,对于flex容器,项目的子元素总宽度大于容器最大宽度。由于flex-wrap的值设置为wrap,所以项目的子元素换行显示。若将其设置为nowrap,这也是初始值,它们将会缩小以适应容器,因为它们使用的是允许缩小的初始Flexbox值。如果项目的子元素无法缩小,使用nowrap会导致溢出,或者缩小程度还不够小。

图 5

简写属性 flex-flow

你可以将两个属性 flex-direction 和 flex-wrap 组合为简写属性 flex-flow。第一个指定的值为 flex-direction ,第二个指定的值为 flex-wrap

在下面的例子中,尝试将第一个值修改为 flex-direction 的允许取值之一,即 row, row-reverse, column 或 column-reverse, 并尝试将第二个指定值修改为 wrap 或 nowrap。

图 6

元素间的对齐和空间分配

Flexbox 的一个关键特性是能够设置 flex 元素沿主轴方向和交叉轴方向的对齐方式,以及它们之间的空间分配。flexbox 之所以能迅速吸引开发者的注意,其中一个原因就是它首次为网页样式居中提供了合适的方案。得益于它提供的合适的垂直居中能力,我们可以很轻松地把一个盒子居中。

通过justify-content属性可以对齐主轴上的元素,使用align-items属性,可以将交叉轴上的元素对齐。

图 0

控制对齐的属性

  • justify-content - 控制主轴(横轴)上所有 flex 项目的对齐。
  • align-items - 控制交叉轴(纵轴)上单行内 flex 项目的对齐。
  • align-content - 控制“多条主轴”的 flex 项目在交叉轴的对齐。
  • align-self - 控制交叉轴(纵轴)上的单个 flex 项目的对齐。

justify-content属性用来使元素在主轴方向上对齐

现在我们可以看一下主轴上的对齐。这里只有一个属性是用于主轴上对齐—— justify-content

图 7

justify-content 属性有和 align-content 一样的属性值:

  • justify-content: flex-start
  • justify-content: flex-end
  • justify-content: center
  • justify-content: space-between
  • justify-content: space-around
  • justify-content: stretch
  • justify-content: space-evenly (没有在 flexbox 特性中定义)

对齐和书写模式

记得这些所有的对齐方法,属性值 flex-start 和 flex-end 是受书写模式的影响的。如果 justify-content 的值是 flex-start 而已你的书写模式是从左到右的话,那么 flex items 就会从 flex container 的左边开始排列。

图 8

反之,则会 flex 项目就会从 flex 容器的右边开始排列。

图 9

align-items 属性可以使单行内元素在交叉轴方向对齐。

这个属性的初始值为stretch,这就是为什么 flex 元素会默认被拉伸到最高元素的高度。实际上,它们被拉伸来填满 flex 容器 —— 最高的元素定义了容器的高度。

图 1

flex items 的高度全都变成一样的原因是 align-items 属性的初始值默认为 stretch 控制交叉轴对齐。

我们可以使用其他的值来控制 flex items 的对齐方式:

  • align-items: flex-start 单行内flex项目的开始端对齐
  • align-items: flex-end 单行内flex项目的结束端对齐
  • align-items: center 单行内flex项目居中对齐
  • align-items: stretch 单行内flex项目撑满flex容器
  • align-items: baseline 单行内flex项目的基线对齐

align-self 用于对齐单个 flex子项

align-items 属性是给所有 flex 项目统一设置 align-self 的对齐属性。这意味着你能给单个 flex 项目明确地声明 align-self 属性。align-self 拥有 align-items 的所有属性值,另外还有一个 auto 能重置自身的值为 align-items 定义的值。

图 2

在上面的一个例子中,flex 容器为 align-items: flex-start,这意思是所有的 flex 项目都在交叉轴方向的开始端对齐。我用 first-child 选择器给第一个 flex 项目设置了 align-self: stretch ;另外一个selected 项目用设置成 align-self: center 。

align-content 用于条主轴行对齐

如果你有一个折行的多条 flex 项目的 flex 容器,你可以使用 align-content 来控制每行之间空间的分配,在这种特定的场景叫做packing flex lines。

要使得 align-content 生效,你需要你的 flex 容器的 height 要大于 flex 项目的可视内容。然后它会将所有的 flex 项目打包成一块之后再对齐剩下的空间。

align-content 属性的值如下:

  • align-content: flex-start
  • align-content: flex-end
  • align-content: center
  • align-content: space-between
  • align-content: space-around
  • align-content: stretch
  • align-content: space-evenly (没有在 Flexbox 特性中定义)

在下面的例子,有一个 400px 高的 flex 容器,能足够地显示 flex 项目。align-content 的值为 space-between 等同于分配 flex 行之间的空间。

图 5

在强调一次我们可以切换我们的 flex-direction 为 column 去观察这个属性的行为是怎样的。和之前一样,我们需要足够的交叉轴空间去显示所有的 flex 项目之外还有有一定的自由空间。

图 6

对齐和 flex-direction

如果你改变 flex-direction 属性,主轴起始线也会改变——例如,使用 row-reverse 代替 row。

图 10

这似乎有一点令人困惑,需要记住的规则就是,当没有设置 flex-direction 时,flex 项目的排列方向与文档语言的文本沿行内轴的排列方向一致。flex-start 就是一个句子中文本的起始处。

图 11

你可以通过设置 flex-direction: column,使弹性项目沿着文档语言的块级轴方向显示。那样话,flex-start 就是文本第一个段落的顶端起始处。

图 12

如果你将 flex-direction 的值改成 row-reverse 或者 column-revers ,那么flex 项目会沿着文档语言的文本书写方向的相反方向,从轴的尾端开始排列。flex-start 就会变为轴的尾端。也就是说,沿着行内轴时,这个位置就是每行文本的换行处;沿着块级轴时,就是文本最后一个段落的底部。

图 13

图 14

在主轴上使用margin 对齐

我们想要处理个别flex 项目在主轴上的对齐,但是没有 justify-items 属性或者 justify-self 属性可用,因为flex 项目会被当成一个组来对齐。然而,我们可以使用 margin 来处理一些flex 项目或者一组flex 项目想和其他flex 项目分离开的对齐情况。

在下面这个在线例子中,flex 项目按默认方式简单地沿行排列,而样式类 push 有设置有 margin-left: auto。你可以尝试删除它,或者把这个类名加到别的flex 项目上,你会看到它是怎样影响flex 项目排列的。
图 15

order 属性

order属性旨在按顺序排列项目。这意味着为项目分配了代表其组的整数。然后,按照该整数(最低的值)首先按照视觉顺序放置项目。如果多个项目具有相同的整数值,则在该组中按照源顺序对项目进行布局。

flex 项目默认 order 值为 0, 因此整数值大于 0 的项目,将会显示在那些未指定 order 值的项目之后。

您还可以使用负值,这很有用。如果要先显示一个项目,并保持所有其他项目的顺序不变,则可以将该项目的顺序设为-1。由于该值小于 0,因此始终会首先显示该项目。

图 16

总结

  1. 容器属性
  • dispaly:

  • flex-direction: 控制主轴方向;

  • flex-warp:控制主轴是否换行;

  • flex-flow:

  • justify-content:控制主轴对齐方式

  • align-item:控制交叉轴行内元素对齐方式

  • align-content:控制“多条主轴”的flex项目在交叉轴的对齐

  1. flex元素属性
  • flex-grow:控制元素拉伸

  • flex-shrink:控制元素压缩

  • flex-basis:控制元素空间大小

  • flex

  • align-self:控制交叉轴上的单个flex项目的对齐

align-items与align-content 的区别

  1. align-items:
  • 作用对象:弹性盒子容器(flex containers);
  • 描述:该属性可以控制弹性容器中成员在当前行内的对齐方式。当成员设置了align-self 属性时,父容器的 align-items 值则不再对它生效;
  1. align-content:
  • 作用对象:弹性盒子容器多行的控制(multi-line flex containers);
  • 描述:当弹性容器在正交轴方向还存在空白时,该属性可以控制其中所有行的对齐方式。Note:该属性无法作用于单行 (flex-wrap: no-wrap) 弹性盒子;
  1. 对比
  • 相同点:都被用来设置对齐行为。
  • 不同点:
    • align-items 的设置对象是行内成员;
    • align-content 的设置对象是所有行,且只有在多行弹性盒子容器中才生效。

align-content 显示效果
图 17

默认设置 display:flex后;
flex-direction:row
flex-warp:nowarp
justify-content:flex-start
align-item:stretch

flex-basis:auto
flex-grow:1
flex-shrink:1
align-self:stretch

flex 元素上的属性

为了更好地控制 flex 元素,有三个属性可以作用于它们:

  • flex-grow
  • flex-shrink
  • flex-basis

在考虑这几个属性的作用之前,需要先了解一下 可用空间 available space 这个概念。这几个 flex 属性的作用其实就是改变了 flex 容器中的可用空间的行为。同时,可用空间对于 flex 元素的对齐行为也是很重要的。

假设在 1 个 500px 的容器中,我们有 3 个 100px 宽的元素,那么这 3 个元素需要占 300px 的宽,剩下 200px 的可用空间。在默认情况下,flexbox 的行为会把这 200px 的空间留在最后一个元素的后面。

图 7

flex-basis

flex-basis 定义了该元素的空间大小(the size of that item in terms of the space),flex 容器里除了元素所占的空间以外的富余空间就是可用空间 available space。该属性的默认值是 auto 。此时,浏览器会检测这个元素是否具有确定的尺寸。在上面的例子中,所有元素都设定了宽度(width)为 100px,所以 flex-basis 的值为 100px。

如果没有给元素设定尺寸,flex-basis 的值采用元素内容的尺寸。这就解释了:我们给只要给 Flex 元素的父元素声明 display: flex ,所有子元素就会排成一行,且自动分配小大以充分展示元素的内容。

flex-grow

flex-grow 若被赋值为一个正整数,flex 元素会以 flex-basis 为基础,沿主轴方向增长尺寸。这会使该元素延展,并占据此方向轴上的可用空间(available space)。如果有其他元素也被允许延展,那么他们会各自占据可用空间的一部分。

如果我们给上例中的所有元素设定 flex-grow 值为 1,容器中的可用空间会被这些元素平分。它们会延展以填满容器主轴方向上的空间。

flex-grow 属性可以按比例分配空间。如果第一个元素 flex-grow 值为 2,其他元素值为 1,则第一个元素将占有 2/4(上例中,即为 200px 中的 100px), 另外两个元素各占有 1/4(各 50px)。

flex-shrink

flex-grow属性是处理 flex 元素在主轴上增加空间的问题,相反flex-shrink属性是处理 flex 元素收缩的问题。如果我们的容器中没有足够排列 flex 元素的空间,那么可以把 flex 元素flex-shrink属性设置为正整数来缩小它所占空间到flex-basis以下。与flex-grow属性一样,可以赋予不同的值来控制 flex 元素收缩的程度 —— 给flex-shrink属性赋予更大的数值可以比赋予小数值的同级元素收缩程度更大。

在计算 flex 元素收缩的大小时,它的最小尺寸也会被考虑进去,就是说实际上 flex-shrink 属性可能会和 flex-grow 属性表现的不一致。

Flex 属性的简写

你可能很少看到 flex-grow,flex-shrink,和 flex-basis 属性单独使用,而是混合着写在 flex 简写形式中。 Flex 简写形式允许你把三个数值按这个顺序书写 — flex-grow,flex-shrink,flex-basis。

图 8

在这个教程中你可能经常会看到这种写法,许多情况下你都可以这么使用。下面是几种预定义的值:

  • flex: initial(flex: 0 1 auto)不能拉伸但可以缩小 flex 元素来防止它们溢出,flex-basis 的值为 auto. Flex 元素尺寸可以是在主维度上设置的,也可以是根据内容自动得到的
  • flex: auto(flex: 1 1 auto)flex 元素在需要的时候既可以拉伸也可以收缩
  • flex: none(flex: 0 0 auto)元素既不能拉伸或者收缩,但是元素会按具有 flex-basis: auto 属性的 flexbox 进行布局
  • flex: 你在教程中常看到的 flex: 1 或者 flex: 2 等等。它相当于flex: 1 1 0 或者 flex: 2 1 0。元素可以在 flex-basis 为 0 的基础上伸缩

https://zhuanlan.zhihu.com/p/24372279

检测对象中属性的存在与否可以通过几种方法来判断。

使用in关键字

如果指定的属性在指定的对象或其原型链中,则 in 运算符返回 true。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const car = { make: 'Honda', model: 'Accord', year: 1998 };

console.log('make' in car);
// Expected output: true

delete car.make;

console.log('make' in car);
// Expected output: false

if ('make' in car === false) {
car.make = 'Suzuki';
}

console.log(car.make);
// Expected output: "Suzuki"


console.log('make' in car);
// Expected output: true

语法

prop in object

参数

  • prop:一个字符串类型或者 symbol 类型的属性名或者数组索引(非 symbol 类型将会强制转为字符串)。

用法

下面的例子演示了一些 in 运算符的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 数组
var trees = new Array("redwood", "bay", "cedar", "oak", "maple");
0 in trees // 返回 true
3 in trees // 返回 true
6 in trees // 返回 false
"bay" in trees // 返回 false (必须使用索引号,而不是数组元素的值)

"length" in trees // 返回 true (length 是一个数组属性)

Symbol.iterator in trees // 返回 true (数组可迭代,只在 ES2015+ 上有效)

// 内置对象
"PI" in Math // 返回 true

// 自定义对象
var mycar = {make: "Honda", model: "Accord", year: 1998};
"make" in mycar // 返回 true
"model" in mycar // 返回 true

in右操作数必须是一个对象值。例如,你可以指定使用String构造函数创建的字符串,但不能指定字符串文字。

1
2
3
4
var color1 = new String("green");
"length" in color1 // 返回 true
var color2 = "coral";
"length" in color2 // 报错 (color2 不是对象)

对被删除或值为 undefined 的属性使用in

如果你使用 delete 运算符删除了一个属性,则 in 运算符对所删除属性返回 false。

1
2
3
4
5
6
7
var mycar = {make: "Honda", model: "Accord", year: 1998};
delete mycar.make;
"make" in mycar; // 返回 false

var trees = new Array("redwood", "bay", "cedar", "oak", "maple");
delete trees[3];
3 in trees; // 返回 false

如果你只是将一个属性的值赋值为undefined,而没有删除它,则 in 运算仍然会返回true。

1
2
3
4
5
6
7
var mycar = {make: "Honda", model: "Accord", year: 1998};
mycar.make = undefined;
"make" in mycar; // 返回 true

var trees = new Array("redwood", "bay", "cedar", "oak", "maple");
trees[3] = undefined;
3 in trees; // 返回 true

继承属性

如果一个属性是从原型链上继承来的,in 运算符也会返回 true。

1
"toString" in {}; // 返回 true

使用对象的hasOwn()方法

如果指定的对象自身有指定的属性,则静态方法 Object.hasOwn() 返回 true。如果属性是继承的或者不存在,该方法返回 false。

备注: Object.hasOwn() 旨在取代 Object.prototype.hasOwnProperty()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var o={x:1};
o.hasOwnProperty("x");    //true,自有属性中有x
o.hasOwnProperty("y");    //false,自有属性中不存在y
o.hasOwnProperty("toString"); //false,这是一个继承属性,但不是自有属性

const object1 = {
prop: 'exists'
};

console.log(Object.hasOwn(object1, 'prop'));
// Expected output: true

console.log(Object.hasOwn(object1, 'toString'));
// Expected output: false

console.log(Object.hasOwn(object1, 'undeclaredPropertyValue'));
// Expected output: false

语法

Object.hasOwn(obj, prop)

  • obj:要测试的 JavaScript 实例对象。
  • prop:要测试属性的 String 类型的名称或者 Symbol。
  • 返回值:如果指定的对象中直接定义了指定的属性,则返回 true;否则返回 false。

建议使用此方法替代 Object.hasOwnProperty(),因为它适用于使用 Object.create(null) 创建的对象且重写了继承的 hasOwnProperty() 方法的对象。尽管可以通过在外部对象上调用 Object.prototype.hasOwnProperty() 解决这些问题,但是 Object.hasOwn() 更加直观。

用法

使用 hasOwn 去测试属性是否存在

以下代码展示了如何确定 example 对象中是否包含名为 prop 的属性。

1
2
3
4
5
6
7
8
9
10
11
12
const example = {};
Object.hasOwn(example, 'prop'); // false——目标对象的属性 'prop' 未被定义

example.prop = 'exists';
Object.hasOwn(example, 'prop'); // true——目标对象的属性 'prop' 已被定义

example.prop = null;
Object.hasOwn(example, 'prop'); // true——目标对象本身的属性存在,值为 null

example.prop = undefined;
Object.hasOwn(example, 'prop'); // true——目标对象本身的属性存在,值为 undefined

直接属性和继承属性

const example = {};
example.prop = ‘exists’;

1
2
3
4
5
6
7
8
9
10
11

// `hasOwn` 静态方法只会对目标对象的直接属性返回 true:
Object.hasOwn(example, 'prop'); // 返回 true
Object.hasOwn(example, 'toString'); // 返回 false
Object.hasOwn(example, 'hasOwnProperty'); // 返回 false

// `in` 运算符对目标对象的直接属性或继承属性均会返回 true:
'prop' in example; // 返回 true
'toString' in example; // 返回 true
'hasOwnProperty' in example; // 返回 true

迭代对象的属性

要迭代对象的可枚举属性,你应该这样使用:

1
2
3
4
5
6

const example = { foo: true, bar: true };
for (const name of Object.keys(example)) {
// …
}

但是如果你使用 for…in,你应该使用 Object.hasOwn() 跳过继承属性:

1
2
3
4
5
6
7
const example = { foo: true, bar: true };
for (const name in example) {
if (Object.hasOwn(example, name)) {
// …
}
}

检查数组索引是否存在

Array 中的元素被定义为直接属性,所以你可以使用 hasOwn() 方法去检查一个指定的索引是否存在:

1
2
3
4
const fruits = ['Apple', 'Banana','Watermelon', 'Orange'];
Object.hasOwn(fruits, 3); // true ('Orange')
Object.hasOwn(fruits, 4); // false——没有定义的

hasOwnProperty 的问题案例

本部分证明了影响 hasOwnProperty 的问题对 hasOwn() 是免疫的。首先,它可以与重新实现的 hasOwnProperty() 一起使用:

1
2
3
4
5
6
7
8
9
10
11
12

const foo = {
hasOwnProperty() {
return false;
},
bar: 'The dragons be out of office',
};

if (Object.hasOwn(foo, 'bar')) {
console.log(foo.bar); //true——重新实现 hasOwnProperty() 不会影响 Object
}

它也可以用于测试使用 Object.create(null) 创建的对象。这些对象不会继承自 Object.prototype,因此 hasOwnProperty() 方法是无法访问的。

1
2
3
4
5
6
7

const foo = Object.create(null);
foo.prop = 'exists';
if (Object.hasOwn(foo, 'prop')) {
console.log(foo.prop); //true——无论对象是如何创建的,它都可以运行。
}

用undefined判断

自有属性和继承属性均可判断。

1
2
3
4
var o={x:1};
o.x!==undefined; //true
o.y!==undefined; //false
o.toString!==undefined //true

该方法存在一个问题,如果属性的值就是undefined的话,该方法不能返回想要的结果,如下

1
2
3
4
var o={x:undefined};
o.x!==undefined; //false,属性存在,但值是undefined
o.y!==undefined; //false
o.toString!==undefined //true

在条件语句中直接判断

1
2
3
4
var o={};
if(o.x) { //如果x是undefine,null,false," ",0或NaN,它将保持不变
o.x+=1;
}

流体系结构

.NET流体系结构由三个概念组成:后端存储,装饰器以及适配器,如图1所示:

Stream类.NET中用于处理流的基类,Stream公开了一组用于读取、写入和定位的标准方法。与数组(数据同时存在于内存中)不同,流连续处理数据(一次一个字节或以可管理大小的块为单位)。因此,无论其后端存储的大小如何,流都可以使用少量的固定内存处理。

图 0

流分为两类:

  • 后端存储流:它们是与特定的后台存储类型连接的流,例如FileStream或者NetworkStream
  • 装饰流:这些流会使用其他的流,并以某种方式转换数据。例如DeflateStream或者CryptoStream。

后台存储是输入输出的终结点,例如文件或者网络连接。准确地说,它可以是以下的一种或者两种:

  • 支持顺序读取字节的源。
  • 支持顺序写入字节的目标。

装饰流具有下列的体系结构优点:

  • 将原来需要后端存储流自己实现例的功能(如压缩或是加密等),剥离出来
  • 当后端存储流被装饰后,后端存储流不再忍受接口变化的痛苦
  • 我们在运行时连接到装饰器
  • 我们可以将装饰器链接在一起

后端存储与装饰器流以字节为单位进行处理。尽管这比较灵活与高效,但是程序通常在更高的层次上进行文本或XML的处理,因此需要适配器,将流封装到一个具有特定方法的类中,来实现文本或xml的处理。例如,一个文本读取器公开了一个ReadLine方法;一个XML写入器公开了WriteAttributes方法。

总结起来就是,后端存储流提供了原始数据;装饰器流提供了透明的二进制转换,例如加密;适配器提供了类型方法来以更高级形式处理数据,例如字符串与XML。图1显示他们之间的关系。要形成一个链,我们只需要将一个对象传递给另一个对象的构造函数。

使用流

抽象的Stream类是所有流的基类。它的方法和属性定义了三种基本的操作:读、写、查找。除此之外,它还定义了一些管理性的任务,例如关闭、刷新(flush)和配置超时时间。

图 1

还有 Read 和 Write 方法的异步版本,它们都返回 Tasks 并可选择接受取消令牌。

读取与写入

流可以支持读取,写入或者两者都支持。如果CanWrite为true,则流支持写入;如果CanRead为true,则流支持读取。

Read方法支持从流中读取数据块并存入数组。他返回所读取的字节数,返回值总是小于或是等于count参数。

如果返回值小于count,则意味着或者是到达了流的结尾,或者是流以更小的块向我们提供数据(在网络流中经常如此)。以上任何一种情况下,数组中的字节余额将保持未写入状态,保留之前的值。

使用 Read,您可以确定仅当该方法返回 0 时您已到达流的末尾。因此,如果您有一个1,000字节的流,以下代码可能无法将其全部读入内存:

1
2
3
// Assuming s is a stream:
byte[] data = new byte [1000];
s.Read (data, 0, data.Length);

Read 方法可以读取 1 到 1,000 字节的任意位置,留下流的剩余部分未读。

下面是读取 1,000 字节流的正确方法:

1
2
3
4
5
6
7
8
9
10
11
byte[] data = new byte [1000];

// bytesRead will always end up at 1000, unless the stream is
// itself smaller in length:

int bytesRead = 0;
int chunkSize = 1;
while (bytesRead < data.Length && chunkSize > 0)
bytesRead +=
chunkSize = s.Read (data, bytesRead, data.Length - bytesRead);

幸运的是,BinaryReader 类型提供了一种更简单的方法来实现相同的结果:

1
byte[] data = new BinaryReader (s).ReadBytes (1000);

如果流的长度小于 1,000 字节,则返回的字节数组反映实际的流大小。如果流是可搜索的,您可以通过将 1000 替换为 (int)s.Length 来读取其全部内容。

ReadByte 方法更简单:它只读取一个字节,返回 -1 以指示流的结尾。 ReadByte 实际上返回一个 int 而不是一个 byte,因为后者不能返回 −1。

Write 和 WriteByte 方法将数据发送到流。如果它们无法发送指定的字节,则会抛出异常。

注意,在Read与Write方法中,offset参数指读取或写在buffer数组中开始的索引,而不是流中的位置。

定位

如果CanSeek方法返回true,则流是可定位的。对于可定位的流(例如文件流),我们可以查询或是修改其Length(通过调用SetLenght),并且在任何时刻修改我们正在读取或是写入的Position。

Position属性是相对于流的起始处的;然而,Seek方法可以使得我们相对于当前位置或是文件的结束处进行移动。

注意,在FileStream上改变Position通常会花费几毫秒的时间。如果我们在一个循环中执行几百万次,框架4.0中新的MemoryMappedFile类是比FileStream更好的选择。

对于不可定位的流(例如加密流),确定其长度的唯一方法就是完全读取。而且,如果我们需要重新读取前面的一部分,我们必须关闭流并重新读取。

关闭与输出缓冲

流在使用之后必须销毁来释放底层资源,例如文件或是套接字句柄。保证关闭流的最简单方法是在using块中实例化流。通常,流遵循标准的销毁语义:

  • Dispose与Close在功能上是相同的
  • 重复销毁或是关闭流不会引起错误

关闭一个装饰器流会同时关闭装饰器以及其后端存储流。对于一个装饰器链,关闭最外层的装饰器(位于链的头部)会关闭整个装饰器链。

某些流在内部会缓冲要写入后端存储的数据或是由后端存储读取的数据来减少读取的来回次数从而改进性能(文件流就是一个好例子)。这意味着我们要写入流的数据也许并没有立即写入后端存储;写入会被延迟直到缓冲区被填满。Flush方法会强制在内部缓冲的数据被立即写入。Flush方法在流被关闭时自动调用的,所以我们不需要执行下面的语句:

s.Flush(); s.Close();

超时

如果CanTimeout返回true,则流支持读写超时。网络支持超时;文件流与内存流则不支持。对于支持超时的流,ReadTimeout与WriteTimeout属性决定了所需要的超时时间,以毫秒计,0则为不超时。Read与Write方法通过抛出异常来表明发生了超时。

线程安全

作为一条规则,流不是线程安全的,意味着两个线程不能同时读取或是写入相同的流,以避免错误。Stream类通过静态的Synchronized方法提供了一个简单的解决办法。这个方法接受一个任意类型的流并返回一个线程安全的包装器。包装器通过获取读取,写入或是定位上的一个排他锁来进行工作,从而保证在任意时刻只有一个线程可以执行这样的操作。

后端存储流

图2显示了.NET框架所提供的关键后端存储流。同时还有一个“空流”,是通过Stream的静态Null域来提供的。

图 2

在以下部分中,我们将描述FileStream与MemoryStream;在本章的最后一部分,我们描述IsolatedStorageStream。

FileStream

创建 FileStream

实例化FileStream的最简单的方法是使用File类型中的静态方法:

1
2
3
FileStream fs1 = File.OpenRead  ("readme.bin");            // Read-only
FileStream fs2 = File.OpenWrite (@"c:\temp\writeme.tmp"); // Write-only
FileStream fs3 = File.Create (@"c:\temp\writeme.tmp"); // Read/write

如果文件已经存在,那么OpenWrite和Create的行为是不同的。Create方法会删除其全部内容,而OpenWrite则会保留流中全部已有内容并将流的起始位置设置为0。如果我们写入的内容比原始文件内容长度还短,则OpenWrite执行之后其文件内容会同时包含新旧内容。

还可以直接实例化一个FileStream。它的构造器支持所有特性,例如允许指定文件名或者底层文件句柄、文件创建和访问模式、共享选项、缓冲选项以及安全性。例如,以下代码会直接打开一个已有文件进行读、写操作,而不会覆盖这个文件:

1
2
var fs = new FileStream("readwrite.tmp", FileMode.Open);

以下的静态方法能够将一个文件一次性读到内存中:

File.ReadAllText(返回字符串)
File.ReadAllLines(返回一个字符串数组)
File.ReadAllBytes(返回一个字节数组)

以下的静态方法能够一次性地写入一个完整的文件:
File.WriteAllText
File.WriteAllLines
File.WriteAllBytes
File.AppendAllText(适用于向日志文件中追加内容)

指定文件名

文件名可以是绝对路径(例如c:\temp\test.txt)也可以是相对当前目录的路径(例如,test.txt或者temp\test.txt)。可以访问Environment.CurrentDirectory属性来获得或者更改当前目录。

AppDomain.CurrentDomain.BaseDirectory属性会返回应用程序的基础目录(base directony),正常情况下它就是可执行文件所在的文件夹。结合使用Path.Combine方法就可以定位该目录下的文件名。

1
2
3
4
5

string baseFolder = AppDomain.CurrentDomain.BaseDirectory;
string tmp = Path.Combine(baseFolder, "readwrite.tmp");
Console.WriteLine(File.Exists(tmp));

指定FileMode

FileStream类型每一个接受文件名的构造器都需要提供FileMode枚举参数。

图 3

如果用于隐藏文件,File.Create 和 FileMode.Create 将抛出异常。要覆盖隐藏文件,您必须删除并重新创建它:

1
2
3
File.Delete ("hidden.txt");
using var file = File.Create ("hidden.txt");
...

在创建FileStream时若只提供文件名和FileMode将会得到一个可读可写的流(但有一种例外)。而如果传入了FileAccess参数,就可以对读写模式进行取舍了:

1
2
public enum FileAccess{Read = 1, Write = 2, ReadWrite = 3}

以下返回一个只读流,相当于调用 File.OpenRead:

1
2
using var fs = new FileStream ("x.bin", FileMode.Open, FileAccess.Read);

FileMode.Append则是一个例外。这个模式只会得到只读的流。相反,如果既要追加内容,又希望支持读写的话,就需要使用FileMode.Open或者FileMode. OpenOrCreate,打开文件,并定位到流的结尾处:

1
2
3
4
5
using (var fs = new FileStream("myFile.bin", FileMode.Open))
{
fs.Seek(0, SeekOrign.End);
...
}

FileStream的高级特性

创建FileSteam时可选的其他参数:

  • FileShare枚举:描述了在完成文件处理之前,若其他进程希望访问该文件,则可以给其他进程授予的访问权限(None、Read、ReadWrite或者Write,其中Read为默认权限)。

  • 内部缓冲区的大小(字节为单位,默认大小为4KB)。

  • 是否由操作系统管理异步I/O的标志。

  • FileSecurity对象,描述给新文件分配的用户角色和权限。

  • FileOptions标志枚举值,其中包括:请求操作系统加密(Encrypted),在文件关闭时自动删除临时文件(DeleteOnClose),以及优化提示(RandomAccess和SequentialScan)。此外还有一个WriteThrough标志可以要求操作系统禁用写后缓存,这适用于事物文件或日志文件的处理。

使用FileShare.ReadWrite打开一个文件可以允许其他进程或用户读写同一个文件。为了避免混乱,我们可以使用以下方法在读或者写之前锁定文件的特定部分。

  • public virtual void Lock (long postion, long length);
  • public virtual void Unlock (long postion, long length);

如果所请求的文件部分已经被锁定了,Lock则抛出异常。这为系统用于基于文件的数据库,例如Access与FoxPro。

MemoryStream

MemoryStream使用数组作为后端存储。这在某种程度上破坏了拥有流的目的,因为整个的后端存储只在内存中存在一次。然而,MemoryStream依然有用;当我们需要随机访问一个不可定位的流时就是一个好例子。如果我们知道源流将是可管理尺寸的,那么我们就可以将其拷贝到MemoryStream中,如下所示:

1
2
var ms = new MemoryStream();
sourceStream.CopyTo (ms);

我们可以通过调用ToArray将一个MemoryStream转换为一个字节数组。

GetBuffer方法通过直接引用底层存储数组可以高效的完成相同的工作;不幸的是,这个数组通常要长于流的实际长度。

PipeStream

PipeStream 可以使用Windows管道协议与另一个进程进行通信。

管道类型有两种:

  • 匿名管道(速度快):支持在同一个计算机中的父进程和子进程之间进行单向通信。

  • 命名管道(更加灵活):允许同一台计算机的任意两个进程之间,或者不同计算机(使用Windows网络)的两个进程间进行双向通信。

管道很适合在同一台计算机进行进程间通信(IPC):它不依赖于任何网络传输(因此没有网络协议开销),性能更好,也不会有防火墙问题。

管道是基于流实现的,因此一个进程会等待接收字节,而另一个进程则负责发送字节。

PipeStream是一个抽象类,它有4个子类。其中两个用于匿名管道而另外两个用于命名管道。

匿名管道:AnonymousPipeServerStream和AnonymousPipeClientStream。

命名管道:NamedPipeServerStream和NamedPipeClientStream。

命名管道

命管道可以让通信各方使用名称相同的管道进行通信。其协议定义了两种不同的角色:客户端与服务器。客户端和服务器之间的通信采用以下方式:

  • 服务器实例化一个NamedPipeServerStream,然后调用WaitForConnection方法。

  • 客户端实例化一个NamedPipeClientStream,然后调用Connect(可提供可选的超时时间)。此后,双方就可以通过读写流进行通信了。

服务端:

1
2
3
4
5
6
7
// server pipe
using(var ns = new NamedPipeServerStream("pipedream"))
{
ns.WaitForConnection();
ns.WriteByte(100);
Console.WriteLine(ns.ReadByte());
}

客户端

1
2
3
4
5
6
7
// client pipe
using (var ns = new NamedPipeClientStream("pipedream"))
{
ns.Connect();
Console.WriteLine(ns.ReadByte());
ns.WriteByte(200);
}

命名管道流默认是双向通信的,因此任何一方都可以读或者写它们的流。这意味着客户端和服务器都必须统一使用一种协议来协调它们的操作,因此双方不能同时发送或者接收消息。

通信双方需要统一每一次传输的数据长度。上面的例子只传输了一个字节,如果要传输更长的数据,管道提供了一种消息传输模式。如果启用了这个模式,调用read的一方可以检查IsMessageComplete来确定消息是否传输完毕。

1
2
3
4
5
6
7
8
9
10
11
// 读取pipstream中的完整消息
static byte[] ReadMessage(PipeStream s)
{
MemoryStream ms = new MemoryStream();
byte[] buffer = new byte[0x1000];
do
{
ms.Write(buffer, 0, s.Read(buffer, 0, buffer.Length));
} while (!s.IsMessageComplete);
return ms.ToArray();
}

在服务器端,在创建流时指定PipeTransm-issionMode.Message就可以激活消息传输(这里还需要传入最大服务端数量):

1
2
3
4
5
6
7
8
9
// server pipe
using(var ns = new NamedPipeServerStream("pipedream", PipeDirection.InOut, 2, PipeTransmissionMode.Message))
{
ns.WaitForConnection();
byte[] msg = Encoding.UTF8.GetBytes("Hello");
ns.Write(msg, 0, msg.Length);
Console.WriteLine(Encoding.UTF8.GetString(ReadMessage(ns)));
}

在客户端,调用Connect之后设置ReadMode即可激活消息传输模式:

1
2
3
4
5
6
7
8
9
10
// client pipe
using (var ns = new NamedPipeClientStream("pipedream"))
{
ns.Connect();
ns.ReadMode = PipeTransmissionMode.Message;
Console.WriteLine(Encoding.UTF8.GetString(ReadMessage(ns)));
byte[] msg = Encoding.UTF8.GetBytes("Hello right back!");
ns.Write(msg, 0, msg.Length);
}

BufferedStream

BufferedStream 装饰或包装另一个具有缓冲功能的流,它是 .NET 中的许多装饰器流类型之一,所有这些都在图 4 中进行了说明。

图 4

缓冲通过减少到后备存储的往返行程来提高性能。以下是我们如何将 FileStream 包装在 20 KB BufferedStream 中:

1
2
3
4
5
6
7
8
// Write 100K to a file:
File.WriteAllBytes ("myFile.bin", new byte [100000]);
using (FileStream fs = File.OpenRead ("myFile.bin"))
using (BufferedStream bs = new BufferedStream (fs, 20000)) //20K buffer
{
bs.ReadByte();
Console.WriteLine (fs.Position); // 20000
}

在这个示例中,由于读取缓冲(bs),底层流(fs)在仅读取1个字节之后读取了20000个字节。我们可以在与FileStream再次交互之前调用ReadByte 19999次。

类似于这个示例,将BufferedStream与FileStream组合并没有太大我价值,因为FileStream已经具有内建的缓冲。他唯一的用处也许就是在已经构建的FileStream上增大缓冲。

关闭BufferedStream会自动关闭底层的后端存储流。

流适配器

Stream只能以字节方式进行处理;要读取或是写入例如字符串,整数或是XML元素这样的数据类型,我们必须借助于适配器。下面是框架所提供的适配:

  • 文本适配器(用于字符串与字符数据):TextReader,TextWriter,StreamReader,StreamWriter,StringReader,StringWriter
  • 二进制适配器(用于基础数据类型,例如int,bool,string与float):BinaryReader,BinaryWriter
  • XML适配器:XmlReader,XmlWriter

图 5

TextReader与TextWriter

TextReader与TextWriter是用于处理字符与字符串的适配器的抽象基类。在框架中,每一个都有两个通用目的的实现:

  • StreamReader/StreamWriter:使用Stream作为原始的数据源,将流字节转换为字符或是字符串
  • StringReader/StringWriter:使用内存字符串实现了TextReader/TextWriter

图 6

表 15-2 按类别列出了 TextReader 的成员。

  • Peek 返回流中的下一个字符而不前进位置。如果在流的末尾,Peek 和 Read 的零参数版本都返回 -1;否则,它们返回一个可以直接转换为 char 的整数。
  • 接受 char[] 缓冲区的 Read 重载在功能上与 ReadBlock 方法相同。
  • ReadLine 读取直到到达 CR(字符 13)或 LF(字符 10),或顺序到达 CR+LF 对。然后它返回一个字符串,丢弃 CR/LF 字符。

图 7

TextWriter 具有类似的写入方法,如表 15-3 所示。

  • Write 和 WriteLine 方法被额外重载以接受每个基本类型以及对象类型。这些方法只是对传入的任何内容调用 ToString 方法(可选地通过在调用方法或构造 TextWriter 时指定的 IFormatProvider)。

  • WriteLine 只是在给定文本后附加 Environment.NewLine。您可以通过 NewLine 属性更改它(这对于与 Unix 文件格式的互操作性很有用)。

StreamReader与StreamWriter

在下面的示例中,StreamWriter向文件写入两行许可证,然后StreamReader读取这个文件:

1
2
3
4
5
6
7
8
9
10
11
12
using (FileStream fs = File.Create ("test.txt"))
using (TextWriter writer = new StreamWriter (fs))
{
writer.WriteLine ("Line1");
writer.WriteLine ("Line2");
}
using (FileStream fs = File.OpenRead ("test.txt"))
using (TextReader reader = new StreamReader (fs))
{
Console.WriteLine (reader.ReadLine()); // Line1
Console.WriteLine (reader.ReadLine()); // Line2
}

因为文本适配器经常与文件进行交互,File提供了静态的CreateText,AppendText以及OpenText来简化处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using (TextWriter writer = File.CreateText ("test.txt"))
{
writer.WriteLine ("Line1");
writer.WriteLine ("Line2");
}
using (TextWriter writer = File.AppendText ("test.txt"))
writer.WriteLine ("Line3");
using (TextReader reader = File.OpenText ("test.txt"))
while (reader.Peek() > -1)
Console.WriteLine (reader.ReadLine());

// Line1
// Line2
// Line3

这同时演示了如何测试文件的结尾(通过reader.Peek())。另一种方法就是一直读取直到reader.ReadLine返回null。

我们也可以读取或是写入其他的类型,例如整数,但是因为TextWriter在我们的类型上调用ToString方法,当我们重新读取时必须分析字符串:

1
2
3
4
5
6
7
8
9
10
using (TextWriter w = File.CreateText ("data.txt"))
{
w.WriteLine (123); // Writes "123"
w.WriteLine (true); // Writes the word "true"
}
using (TextReader r = File.OpenText ("data.txt"))
{
int myInt = int.Parse (r.ReadLine()); // myInt == 123
bool yes = bool.Parse (r.ReadLine()); // yes == true
}

字符编码

TextReader与TextWriter仅是不具有到流或是后端存储连接的抽象类。然而,StreamReader与StreamWriter类型则连接到底层面向字节的流,所以他们必须在字符与字节之间进行转换。他们是通过System.Text名字空间听 Encoding类来完成的,我们可以在构建StreamReader或是StreamWriter时选择。如果我们没有选择,则使用默认的UTF-8编码。

最简单的编码是ASCII编码,因为每一个字符由一个字节表示。ASCII编码将Unicode集合中的前127个字符映射为单个字节,转换我们在US风格的键盘上所看到的字符。大多数其他的字符,包括特殊符号以及非英语字符不能被表示,并被转换为□字符。默认的UTF-8编码可以映射所有的Unicode字符,但是他更为复杂。为了与ASCII兼容,前127个字符被编码为单个字节;其余的字符被编码为变化的字节数(通常是两个或是三个)。例如:

1
2
3
4
5
using (TextWriter w = File.CreateText ("but.txt"))    // Use default UTF-8
w.WriteLine ("but-"); // encoding.
using (Stream s = File.OpenRead ("but.txt"))
for (int b; (b = s.ReadByte()) > ?1;)
Console.WriteLine (b);

单词“but”之后并不是一个标准的连字符,而是一个更长的em dash字符(—),U+2014。这不会使得我们的书本编辑器遇到麻烦。让我们看一下其输出:

1
2
3
4
5
6
7
8
98     // b
117 // u
116 // t
226 // em dash byte 1 Note that the byte values
128 // em dash byte 2 are >= 128 for each part
148 // em dash byte 3 of the multibyte sequence.
13 // <CR>
10 // <LF>

由于em dash位于Unicode集合中前127个字符之外,他要求更多个的字节来进行UTF-8编码(在这个示例中为三个)。

UTF-8足够表示西方字符,因为大多数字符仅需要一个字节。通过简单的忽略127以上的字符,他可以很容易的转换为ASCII字符。其缺点是在流中定位比较麻烦,因为一个字符的位置并不与流中其字节位置相对应。

另一种方法是UTF-16。下面是我们使用UTF-16编写相同的字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using (Stream s = File.Create ("but.txt"))
using (TextWriter w = new StreamWriter (s, Encoding.Unicode))
w.WriteLine ("but-");

foreach (byte b in File.ReadAllBytes ("but.txt"))
Console.WriteLine (b);

输出如下:

255 // Byte-order mark 1
254 // Byte-order mark 2
98 // 'b' byte 1
0 // 'b' byte 2
117 // 'u' byte 1
0 // 'u' byte 2
116 // 't' byte 1
0 // 't' byte 2
20 // '--' byte 1
32 // '--' byte 2
13 // <CR> byte 1
0 // <CR> byte 2
10 // <LF> byte 1
0 // <LF> byte 2

从技术上讲,UTF-16 每个字符使用两个或四个字节(分配或保留了近一百万个 Unicode 字符,因此两个字节并不总是足够的)。但是,由于 C# char 类型本身只有 16 位宽,UTF-16 编码将始终为每个 .NET char 使用恰好两个字节。这使得跳转到流中的特定字符索引变得容易。

UTF-16 使用双字节前缀来标识字节对是以“小端”还是“大端”顺序写入的(最低有效字节在前或最高有效字节在前)。默认的小端顺序是基于 Windows 的系统的标准顺序。

StringReader与StringWriter

StringReader与StringWriter并没有封装流;相反,他们使用字符串或是StringBuilder作为底层数据存储。这就意味着并不需要字节转换-事实上,除了我们使用字符串或是StringBuilder结合索引变量很容易实现的事情以外,这个类并不能做其他事情。他们的优点是与StreamReader/StringWriter共享相同的基类。例如,假设我们有一个包含XML的字符串,并且希望使用XmlReader进行分析。XmlReader.Create可以接受下列中的一个:

  • URI
  • TextReader

那么,我们如何对字符串进行 XML 解析?因为 StringReader 是 TextReader 的子类,所以我们很幸运。我们可以实例化并传入一个 StringReader,如下所示:

1
XmlReader r = XmlReader.Create (new StringReader (myString));

二进制适配器

BinaryReader与BinaryWriter可以读取与写入本地数据类型:bool,byte,char,decimal,float,double,short,int,long,sbyte,ushort,unit与ulong,以及string和基础数据类型的数组。

与 StreamReader 和 StreamWriter 不同,二进制适配器有效地存储原始数据类型,因为它们在内存中表示。因此,一个 int 使用四个字节; double 使用八个字节。字符串是通过文本编码(与 StreamReader 和 StreamWriter 一样)写入的,但带有长度前缀,这样就可以在不需要特殊分隔符的情况下读回一系列字符串。

假定我们有一个简单的类型,定义如下:

1
2
3
4
5
6
public class Person
{
public string Name;
public int Age;
public double Height;
}

我们可以将以下方法添加到 Person 以使用二进制适配器将其数据保存到流中/从流中加载数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void SaveData (Stream s)
{
var w = new BinaryWriter (s);
w.Write (Name);
w.Write (Age);
w.Write (Height);
w.Flush(); // Ensure the BinaryWriter buffer is cleared.
// We won't dispose/close it, so more data
} // can be written to the stream.

public void LoadData (Stream s)
{
var r = new BinaryReader (s);
Name = r.ReadString();
Age = r.ReadInt32();
Height = r.ReadDouble();
}

BinaryReader 也可以读入字节数组。下面读取一个可搜索流的全部内容:

1
2
byte[] data = new BinaryReader (s).ReadBytes ((int) s.Length);

这比直接从流中读取更方便,因为它不需要循环来确保所有数据都已被读取。

关闭与销毁流适配器

关闭流适配器时我们下列四个选择:

  • 仅关闭适配器
  • 关闭适配器,然后关闭流
  • (对于写入)调用适配器的Flush()方法(输出缓冲),然后关闭流
  • (对于读取)仅关闭流

选项1与2在语义是上相同的,因为关闭适配器会自动关闭底层流。当我们嵌入using语句时,我们隐式的选择了选项2:

1
2
3
using (FileStream fs = File.Create ("test.txt"))
using (TextWriter writer = new StreamWriter (fs))
writer.WriteLine ("Line");

由于嵌入语句是由里向外销毁,所以适配器被首先关闭,然后是流。而且,如果在适配器的构造器中抛出异常,流仍然关闭。嵌入的using语句很难遇到错误。

1
2
3
4
5
6
7
8
using (FileStream fs = new FileStream ("test.txt", FileMode.Create))
{
StreamWriter writer = new StreamWriter (fs);
writer.WriteLine ("Hello");
writer.Flush();
fs.Position = 0;
Console.WriteLine (fs.ReadByte());
}

在这里我们写入文件,重新定位流,并且在关闭流之前读取第一个字节。如果我们销毁StreamWriter,他也会关闭底层的FileStream,从而例程后续的读取操作失败。这样做的限制则是我们调用Flush来保证StreamWriter的缓冲被写入底层流中。

StreamReader/StreamWriter 上还有一个构造函数,指示它在处理后保持流打开。因此,我们可以将前面的例子重写如下:

1
2
3
4
5
6
7
8
using (var fs = new FileStream ("test.txt", FileMode.Create))
{
using (var writer = new StreamWriter (fs, new UTF8Encoding (false, true), 0x400, true))
writer.WriteLine ("Hello");
fs.Position = 0;
Console.WriteLine (fs.ReadByte());
Console.WriteLine (fs.Length);
}

总结

  • FileSteam:内部缓冲区的大小(字节为单位,默认大小为4KB)

文件是一个由字节组成的有序的命名集合,它具有永久存储。在处理文件时,你将处理目录路径、磁盘存储、文件和目录名称。

文件和目录

System.IO 命名空间提供了一组用于执行“实用”文件和目录操作的类型,例如复制和移动、创建目录以及设置文件属性和权限。对于大多数功能,您可以在两个类中选择一个,一个提供静态方法,另一个提供实例方法:

  • 静态类:File 和 Directory

  • 实例方法类:FileInfo 和 DirectoryInfo

另外,还有一个名为Path的静态类。这个类对于文件或是目录并没有什么;相反,他为文件名与目录路径提供了字符串处理的方法。Path同时辅助临时文件处理。

下面是一些常用的文件和目录类:

  • File - 提供用于创建、复制、删除、移动和打开文件的静态方法,并可帮助创建 FileStream 对象。
  • FileInfo - 提供用于创建、复制、删除、移动和打开文件的实例方法,并可帮助创建 FileStream 对象。
  • Directory - 提供用于创建、移动和枚举目录和子目录的静态方法。
  • DirectoryInfo - 提供用于创建、移动和枚举目录和子目录的实例方法。
  • Path - 提供用于以跨平台的方式处理目录字符串的方法和属性。

File类

File是一个静态类,其方法接受文件名。文件名可以相对于当前目录或是具有目录的绝对路径。该类所具有的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool Exists (string path);      // Returns true if the file is present
void Delete (string path);
void Copy (string sourceFileName, string destFileName);
void Move (string sourceFileName, string destFileName);
void Replace (string sourceFileName, string destinationFileName,
string destinationBackupFileName);
FileAttributes GetAttributes (string path);
void SetAttributes (string path, FileAttributes fileAttributes);
void Decrypt (string path);
void Encrypt (string path);
DateTime GetCreationTime (string path); // UTC versions are
DateTime GetLastAccessTime (string path); // also provided.
DateTime GetLastWriteTime (string path);
void SetCreationTime (string path, DateTime creationTime);
void SetLastAccessTime (string path, DateTime lastAccessTime);
void SetLastWriteTime (string path, DateTime lastWriteTime);
FileSecurity GetAccessControl (string path);
FileSecurity GetAccessControl (string path,
AccessControlSections includeSections);
void SetAccessControl (string path, FileSecurity fileSecurity);

如果目标文件已经存在,则Move会抛出异常;Replace则不会。两个方法都允许文件被重命名以及移动到另一个目录中。

如果文件被标记为只读,则会抛出UnauthorizedAccessException;如果我们通过调用GetAttributes来识别属性。下面是GetAttributes返回的FileAttribute枚举成员:

1
2
3
Archive, Compressed, Device, Directory, Encrypted,
Hidden, Normal, NotContentIndexed, Offline, ReadOnly,
ReparsePoint, SparseFile, System, Temporary

这个枚举中的成员是可组合的。下面显示如何修改文件的一个属性而不影响其他的属性:

1
2
3
4
5
6
7
8
9
string filePath = @"c:\temp\test.txt";
FileAttributes fa = File.GetAttributes (filePath);
if ((fa & FileAttributes.ReadOnly) > 0)
{
fa ^= FileAttributes.ReadOnly;
File.SetAttributes (filePath, fa);
}
// Now we can delete the file, for instance:
File.Delete (filePath);

Stream(抽象基类)支持读取和写入字节。 所有表示流的类都继承自Stream类。 Stream类及其派生类提供数据源和存储库的常见视图,使程序员不必了解操作系统和基础设备的具体细节。

流涉及三个基本操作:

  • 读取 - 将数据从流读取到数据结构(如字节数组)中。

  • 写入 - 将数据从数据源写入到流中。

  • 查找 - 对流中的当前位置进行查询和修改。

下面是一些常用的流类:

  • FileStream - 用于对文件进行读取和写入操作。

  • MemoryStream - 用于对内存进行读取和写入操作。

  • BufferedStream - 用于改进读取和写入操作的性能。

  • NetworkStream - 用于通过网络套接字进行读取和写入。

  • IsolatedStorageFileStream - 用于对独立存储中的文件进行读取或写入操作。

  • PipeStream - 用于通过匿名和命名管道进行读取和写入。

  • CryptoStream - 用于将数据流链接到加密转换。

读取器和编写器:
System.IO 命名空间还提供用于在流中读取和写入已编码字符的类型。 通常,流用于字节输入和输出。 读取器和编写器 处理编码字符与字节之间的来回转换,以便流可以完成操作。 每个读取器和编写器类都与流关联,可以通过类的 BaseStream 属性进行检索。

下面是一些常用的读取器和编写器类:

  • BinaryReader 和 BinaryWriter - 用于将基元数据类型作为二进制值进行读取和写入。

  • StreamReader 和 StreamWriter - 用于通过使用编码值在字符和字节之间来回转换来读取和写入字符。

  • StringReader 和 StringWriter - 用于从字符串读取字符以及将字符写入字符串中。

  • TextReader 和 TextWriter - 用作其他读取器和编写器(读取和写入字符和字符串,而不是二进制数据)的抽象基类。

Stream 类

继承自Stream类的一些更常用的流包括 FileStream、 和 MemoryStream。根据不同数据源类型,继承自Stream类的流可能仅支持Stream类中的某些功能。

属性

  • CanRead
    当在派生类中重写时,获取指示当前流是否支持读取的值。

  • CanSeek
    当在派生类中重写时,获取指示当前流是否支持查找功能的值。

  • CanTimeout
    获取一个值,该值确定当前流是否可以超时。

  • CanWrite
    当在派生类中重写时,获取指示当前流是否支持写入功能的值。

  • Length
    当在派生类中重写时,获取流长度(以字节为单位)。

  • Position
    当在派生类中重写时,获取或设置当前流中的位置。

很多asp.net项目中文件或图片上传中很多朋友会经历过这样一个痛苦:Stream对象被缓存了,导致了Position属性在流中无法
找到正确的位置,这点会让人抓狂,其实解决这个问题很简单,我们每次使用流前必须将Stream.Position设置成0就行了,但是这还不能根本上解决问题,最好的方法就是用Using语句将流对象包裹起来,用完后关闭回收即可。

  • ReadTimeout
    获取或设置一个值(以毫秒为单位),该值确定流在超时前将尝试读取的时间。

  • WriteTimeout
    获取或设置一个值(以毫秒为单位),该值确定流在超时前将尝试写入多长时间。

方法

  • Flush()
    当在派生类中重写时,将清除该流的所有缓冲区,并使得所有缓冲数据被写入到基础设备。

某些流实现对基础数据执行本地缓冲以提高性能。 对于此类流,可以使用 Flush 或 FlushAsync 方法来清除缓冲区,并确保所有数据都已写入基础数据源或存储库。

当使用 StreamWriter 或 BinaryWriter 类时,不要刷新 Stream 基对象。而应使用该类的 Flush 或 Close 方法,此方法确保首先将该数据刷新至基础流,然后再将其写入文件。

  • int Read(Byte[] buffer, Int32 offset, Int32 count)
    当在派生类中重写时,从当前流读取字节序列,并将此流中的位置提升读取的字节数。

    这个方法包含了3个关键的参数:

    • buffer 缓冲字节数组,此数组中 offset 和 (offset + count - 1) 之间的值被从当前源中读取的字节所替换;

    • offset 位移偏量,从buffer的offset处开始存储从流中读取的数据;

    • count 读取字节个数,要从当前流中最多读取的字节数;

    返回值:读入缓冲区中的总字节数,总字节数可能小于请求的字节数,如果已到达流结尾,则为零 (0);

  • Seek(long offset , SeekOrigin origin)
    当在派生类中重写时,设置当前流中的位置。

    • offset: 相对于 origin 参数的字节偏移量。
    • origin: SeekOrigin 类型的值,指示用于获取新位置的参考点。

    如果 offset 为负,则要求新位置位于 origin 指定的位置之前,其间隔相差 offset 指定的字节数。如果 offset 为零 (0),则要求新位置位于由 origin 指定的位置处。

    如果 offset 为正,则要求新位置位于 origin 指定的位置之后,其间隔相差 offset 指定的字节数.

    • Stream. Seek(-3,Origin.End); 表示在流末端往前数第3个位置

    • Stream. Seek(0,Origin.Begin); 表示在流的开头位置

    • Stream. Seek(3,Orig`in.Current); 表示在流的当前位置往后数第三个位置

  • void Write (byte[] buffer, int offset, int count); 向当前流中写入字节序列,并将此流中的当前位置提升写入的字节数。

    • buffer: 字节数组。 此方法将 count 个字节从 buffer 复制到当前流

    • offset: buffer 中的字节偏移量,从此处开始将字节复制到当前流;

    • count: 要写入当前流的字节数

    如果写入操作成功,则流中的位置将按写入的字节数前进。 如果发生异常,则流中的位置保持不变。

TextReader

是一个抽象类。 因此,不要在代码中实例化它。 派生类(StreamReader 和 StringReader)必须至少实现 Peek() 和 Read() 方法,才能成为TextReader有用的实例。

此类型实现 IDisposable 接口。 使用完派生自此类型的任何类型后,应直接或间接释放它。 若要直接释放类型,请在 try/catch 块中调用其 Dispose 方法。 若要间接释放类型,请使用 using(在 C# 中)等语言构造

方法

  • void Close ():关闭 TextReader 并释放与该 TextReader 关联的所有系统资源;

  • void Dispose ():释放由 TextReader 对象使用的所有资源;假如TextReader中持有stream或其他对象,当TextReader执行了Dispose方法时,stream对象也被回收了;

  • int Peek ():读取下一个字符,而不更改读取器状态或字符源。 返回下一个可用字符,而实际上并不从读取器中读取此字符。

    返回值:一个表示下一个要读取的字符的整数;如果没有更多可读取的字符或该读取器不支持查找,则为 -1。
    该方法 Peek 返回整数值,以确定文件末尾还是发生了另一个错误。 这样,用户就可以先检查返回的值是否为 -1,然后再将其强制转换为 Char 类型。

  • int Read ():读取文本读取器中的下一个字符并使该字符的位置前移一个字符。

返回值:文本读取器中的下一个字符,或为 -1(如果没有更多可用字符)。 默认实现将返回 -1。

read()方法使指针指向下个字符,但是peek()还是指向原来那个字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

string text = "abc\nabc";


using (TextReader reader = new StringReader(text))
{
while (reader.Peek() != -1)
{
Console.WriteLine("Peek = {0}", (char)reader.Peek());
Console.WriteLine("Read = {0}", (char)reader.Read());
}
reader.Close();
}

using (TextReader reader = new StringReader(text))
{
char[] charBuffer = new char[3];
int data = reader.ReadBlock(charBuffer, 0, 3);
for (int i = 0; i < charBuffer.Length; i++)
{
Console.WriteLine("通过readBlock读出的数据:{0}", charBuffer[i]);
}
reader.Close();
}

using (TextReader reader = new StringReader(text))
{
string lineData = reader.ReadLine();
Console.WriteLine("第一行的数据为:{0}", lineData);
reader.Close();
}

using (TextReader reader = new StringReader(text))
{
string allData = reader.ReadToEnd();
Console.WriteLine("全部的数据为:{0}", allData);
reader.Close();
}

Console.ReadLine();

StreamReader

实现一个 TextReader,使其以一种特定的编码从字节流中读取字符。

StreamReader 设计用于特定编码中的字符-Char输入,而 Stream 类设计用于*字节-byte**输入和输出。 用于 StreamReader 从标准文本文件读取信息行。

StreamReader 除非另行指定,否则默认为 UTF-8 编码,而不是默认为当前系统的 ANSI 代码页。 UTF-8 正确处理 Unicode 字符,并在操作系统的本地化版本上提供一致的结果。

如果使用CurrentEncoding属性获取当前字符编码 ,则值在执行第一个 Read 方法之后才可靠,因为编码自动检测在首次调用方法之前不会完成。

构造函数

  • StreamReader(Stream) :为指定的流初始化 StreamReader 类的新实例。
  • StreamReader(Stream, Encoding):用指定的字符编码为指定的流初始化 StreamReader 类的一个新实例。
  • StreamReader(String, Encoding):用指定的字符编码,为指定的文件名初始化 StreamReader 类的一个新实例。

    这里的string对象不是简单的字符串而是具体文件的地址,然后根据用户选择编码去读取流中的数据

属性

  • BaseStream:返回基础流。
    1
    2
    3
    4
    FileStream fs = new FileStream ( "D:\\TextReader.txt", FileMode.Open , FileAccess.Read ) ; 
    StreamReader sr= new StreamReader ( fs ) ;
    //本例中的BaseStream就是FileStream
    sr.BaseStream.Seek (0 , SeekOrigin.Begin ) ;
  • CurrentEncoding:获取当前 StreamReader 对象正在使用的当前字符编码
  • EndOfStream:获取一个值,该值指示当前的流位置是否在流结尾,如果当前流位置位于流的末尾,则为 true;否则为 false。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

//文件地址
string txtFilePath = "D:\\TextReader.txt";
//定义char数组
char[] charBuffer2 = new char[3];

//利用FileStream类将文件文本数据变成流然后放入StreamReader构造函数中
using (FileStream stream = File.OpenRead(txtFilePath))
{
using (StreamReader reader = new StreamReader(stream))
{
//StreamReader.Read()方法
DisplayResultStringByUsingRead(reader);
}
}

using (FileStream stream = File.OpenRead(txtFilePath))
{
//使用Encoding.ASCII来尝试下
using (StreamReader reader = new StreamReader(stream, Encoding.ASCII, false))
{
//StreamReader.ReadBlock()方法
DisplayResultStringByUsingReadBlock(reader);
}
}

//尝试用文件定位直接得到StreamReader,顺便使用 Encoding.Default
using (StreamReader reader = new StreamReader(txtFilePath, Encoding.Default, false, 123))
{
//StreamReader.ReadLine()方法
DisplayResultStringByUsingReadLine(reader);
}

//也可以通过File.OpenText方法直接获取到StreamReader对象
using (StreamReader reader = File.OpenText(txtFilePath))
{
//StreamReader.ReadLine()方法
DisplayResultStringByUsingReadLine(reader);
}

Console.ReadLine();


/// <summary>
/// 使用StreamReader.Read()方法
/// </summary>
/// <param name="reader"></param>
public static void DisplayResultStringByUsingRead(StreamReader reader)
{
int readChar = 0;
string result = string.Empty;
while ((readChar = reader.Read()) != -1)
{
result += (char)readChar;
}
Console.WriteLine("使用StreamReader.Read()方法得到Text文件中的数据为 : {0}", result);
}

/// <summary>
/// 使用StreamReader.ReadBlock()方法
/// </summary>
/// <param name="reader"></param>
public static void DisplayResultStringByUsingReadBlock(StreamReader reader)
{
char[] charBuffer = new char[10];
string result = string.Empty;
reader.ReadBlock(charBuffer, 0, 10);
for (int i = 0; i < charBuffer.Length; i++)
{
result += charBuffer[i];
}
Console.WriteLine("使用StreamReader.ReadBlock()方法得到Text文件中前10个数据为 : {0}", result);
}


/// <summary>
/// 使用StreamReader.ReadLine()方法
/// </summary>
/// <param name="reader"></param>
public static void DisplayResultStringByUsingReadLine(StreamReader reader)
{
int i = 1;
string resultString = string.Empty;
while ((resultString = reader.ReadLine()) != null)
{
Console.WriteLine("使用StreamReader.Read()方法得到Text文件中第{1}行的数据为 : {0}", resultString, i);
i++;
}
}

TextWriter

TextWriter是抽象基类,子类 StreamWriter 和 StringWriter,分别将字符写入流和字符串。 派生类必须实现 Write(Char) 方法,才能创建一个有用的实例 TextWriter。

构造函数

  • TextWriter():初始化 TextWriter 类的新实例。

  • TextWriter(IFormatProvider):使用指定的格式提供程序初始化 TextWriter 类的新实例。

    使用此构造函数创建的实例,在调用 Write 和 WriteLine 方法时使用FormatProvider属性值,设置的区域性特定格式。

属性

  • Encoding:当在派生类中重写时,返回用来写输出的该字符编码。

  • FormatProvider:获取控制格式设置的对象。

  • NewLine:获取或设置由当前 TextWriter 使用的行结束符字符串。

StreamWriter

StreamWriter 以一种特定的编码向流中写入字符。 除非另外指定,否则默认为使用UTF8Encoding实例 。 UTF8Encoding实例在构造时没有字节顺序标记 (BOM) ,因此其 GetPreamble 方法返回一个空字节数组。 此构造函数的默认 UTF-8 编码对无效字节引发异常。 此行为不同于属性中的编码对象提供的行为 Encoding.UTF8 。 若要指定一个 BOM 并确定无效字节是否引发了异常,请使用接受编码对象作为参数的构造函数。

构造函数

  • StreamWriter(Stream):使用 UTF-8 编码及默认的缓冲区大小,为指定的流初始化 StreamWriter 类的新实例。

  • StreamWriter(Stream, Encoding):使用指定的编码及默认的缓冲区大小,为指定的流初始化 StreamWriter 类的新实例。

  • StreamWriter (string path, bool append):用默认编码和缓冲区大小,为指定的文件初始化 StreamWriter 类的一个新实例。

    若要追加数据到该文件中,append为 true;若要覆盖该文件,append为 false。 如果指定的文件不存在,该参数无效,且构造函数将创建一个新文件。

属性

  • AutoFlush:获取或设置一个值,该值指示 StreamWriter 在每次调用 Write(Char) 之后是否都将其缓冲区刷新到基础流。

  • BaseStream:获取同后备存储连接的基础流。

  • Encoding:获取在其中写入输出的 Encoding。

  • FormatProvider:获取控制格式设置的对象。

  • NewLine:获取或设置由当前 TextWriter 使用的行结束符字符串。

FileStream

使用FileStream类读取、写入、打开和关闭文件系统上的文件,并操作其他与文件相关的操作系统句柄,包括管道、标准输入和标准输出。 可以使用 Read、 Write、 CopyTo和 Flush 方法执行同步操作,或使用 ReadAsync、 WriteAsync、 CopyToAsync和 FlushAsync 方法执行异步操作。 使用异步方法执行资源密集型文件操作,而不会阻止主线程。

属性 IsAsync 检测文件句柄是否已异步打开。 使用具有 isAsync、 或 options 参数的构造函数创建FileStream类的实例时,useAsync可以指定此值。 当IsAsync属性为 true时,流利用重叠的 I/O 以异步方式执行文件操作。

当IsAsync属性为 false 并且你调用异步读取和写入操作时,UI 线程仍不会被阻止,但实际 I/O 操作是同步执行的。

方法 Seek 支持对文件的随机访问。 Seek 允许将读/写位置移动到文件中的任何位置。

当对象 FileStream 在其句柄上没有独占保留时,另一个线程可以同时访问文件句柄,并更改与文件句柄关联的操作系统文件指针的位置。 在这种情况下,对象中的 FileStream 缓存位置和缓冲区中的缓存数据可能会受到威胁。 对象 FileStream 定期对访问缓存缓冲区的方法执行检查,以确保操作系统的句柄位置与对象使用的 FileStream 缓存位置相同。

如果在调用 Read 方法时检测到句柄位置的意外更改,.NET Framework放弃缓冲区的内容,并从文件中再次读取流。 这可能会影响性能,具体取决于文件的大小以及可能影响文件流位置的任何其他进程。

如果在调用 Write 方法时检测到句柄位置的意外更改,则会丢弃缓冲区的内容并 IOException 引发异常。

构造函数

  • FileStream(SafeFileHandle, FileAccess):使用指定的读/写权限为指定的文件句柄初始化 FileStream 类的新实例。

    SafeFileHandle
    当前 FileStream 对象将封装的文件的文件句柄。

FileAccess
枚举值的按位组合,它用于设置 FileStream 对象的 CanRead 和 CanWrite 属性。

  • FileStream(String, FileMode, FileAccess, FileShare, Int32, FileOptions):使用指定的路径、创建模式、读/写和共享权限、其他 FileStreams 可以具有的对此文件的访问权限、缓冲区大小和附加文件选项初始化 FileStream 类的新实例。

String: 当前 FileStream 对象将封装的文件的相对路径或绝对路径。

FileMode: 用于确定文件的打开或创建方式的枚举值之一。

FileAccess: 枚举值的按位组合,这些枚举值确定 FileStream 对象访问文件的方式。 该常数还可以确定由 FileStream 对象的 CanRead 和 CanWrite 属性返回的值。 如果 path 指定磁盘文件,则 CanSeek 为 true。

FileShare: 枚举值的按位组合,这些枚举值确定进程共享文件的方式。

Int32: 一个大于零的正 Int32 值,表示缓冲区大小。 默认缓冲区大小为 4096
FileOptions: 枚举值的按位组合,它用于指定其他文件选项

参数说明

  • FileMode:指定操作系统打开文件的方式。

    Append: 若存在文件,则打开该文件并查找到文件尾,或者创建一个新文件。 FileMode.Append 只能与 FileAccess.Write 一起使用。 试图查找文件尾之前的位置时会引发 IOException 异常,并且任何试图读取的操作都会失败并引发 NotSupportedException 异常。

    Create: 指定操作系统应创建新文件。 如果此文件已存在,则会将其覆盖。 这需要 Write 权限。
    FileMode.Create 等效于这样的请求:如果文件不存在,则使用 CreateNew;否则使用 Truncate。 如果该文件已存在但为隐藏文件,则将引发 UnauthorizedAccessException异常。

    CreateNew: 指定操作系统应创建新文件。 这需要 Write 权限。 如果文件已存在,则将引发 IOException异常。

    Open: 指定操作系统应打开现有文件。 打开文件的能力取决于 FileAccess 枚举所指定的值。 如果文件不存在,引发一个 FileNotFoundException 异常。

    OpenOrCreate: 指定操作系统应打开文件(如果文件存在);否则,应创建新文件。 如果用 FileAccess.Read 打开文件,则需要 Read权限。 如果文件访问为 FileAccess.Write,则需要 Write权限。 如果用 FileAccess.ReadWrite 打开文件,则同时需要 Read 和 Write权限

    Truncate: 指定操作系统应打开现有文件。 该文件被打开时,将被截断为零字节大小。 这需要 Write 权限。 尝试从使用 FileMode.Truncate 打开的文件中进行读取将导致 ArgumentException 异常。

  • FileAccess: 定义文件的读取、写入或读/写访问权限的常量。

Read: 对文件的读访问。 可从文件中读取数据。 与 Write 组合以进行读写访问。

ReadWrite: 对文件的读写访问权限。 可从文件读取数据和将数据写入文件。

Write: 文件的写访问。 可将数据写入文件。 与 Read 组合以进行读写访问。

  • FileShare: 包含用于控制其他 FileStream 对象对同一文件可以具有的访问类型的常数。

Delete: 允许随后删除文件。

Inheritable: 使文件句柄可由子进程继承。 Win32 不直接支持此功能。

None: 谢绝共享当前文件。 文件关闭前,打开该文件的任何请求(由此进程或另一进程发出的请求)都将失败。

Read: 允许随后打开文件读取。 如果未指定此标志,则文件关闭前,任何打开该文件以进行读取的请求(由此进程或另一进程发出的请求)都将失败。 但是,即使指定了此标志,仍可能需要附加权限才能够访问该文件。

ReadWrite: 允许随后打开文件读取或写入。 如果未指定此标志,则文件关闭前,任何打开该文件以进行读取或写入的请求(由此进程或另一进程发出)都将失败。 但是,即使指定了此标志,仍可能需要附加权限才能够访问该文件。

Write: 允许随后打开文件写入。 如果未指定此标志,则文件关闭前,任何打开该文件以进行写入的请求(由此进程或另一进过程发出的请求)都将失败。 但是,即使指定了此标志,仍可能需要附加权限才能够访问该文件。

  • FileOptions

Asynchronous: 指示文件可用于异步读取和写入

DeleteOnClose: 指示当不再使用某个文件时,自动删除该文件。

Encrypted: 指示文件是加密的,只能通过用于加密的同一用户帐户来解密。

None: 指示在生成 FileStream 对象时,不应使用其他选项。

RandomAccess: 指示随机访问文件。 系统可将此选项用作优化文件缓存的提示。

SequentialScan: 指示按从头到尾的顺序访问文件。 系统可将此选项用作优化文件缓存的提示。 如果应用程序移动用于随机访问的文件指针,可能不发生优化缓存,但仍然保证操作的正确性。 如果指定此标志,可提升某些案例中的性能。

WriteThrough: 指示系统应通过任何中间缓存、直接写入磁盘。

指定 FileOptions.SequentialScan 标志可以提高使用顺序访问读取大型文件的应用程序的性能。 对于主要按顺序读取大型文件,但偶尔跳过小范围字节的应用程序,性能提升可能更为明显。

属性

  • IsAsync:获取一个值,它指示 FileStream 是异步打开还是同步打开的。

  • Length:获取流的长度(以字节为单位)。

  • Name:获取 FileStream 中已打开的文件的绝对路径。

  • Position:获取或设置此流的当前位置。

  • SafeFileHandle:获取 SafeFileHandle 对象,它代表当前 FileStream 对象所封装的文件的操作系统文件句柄。

方法

属于FileStream独有的方法

  • FileSecurity GetAccessControl(FileStream): 返回文件的安全信息。

    FileStream: 一个要从中获取安全信息的现有文件

  • void SetAccessControl(FileSecurity fileSecurity): 和GetAccessControl很相似,ACL技术会在以后单独介绍

  • Lock (long position, long length);防止其他进程读取或写入 FileStream。

这个Lock方法和线程中的Look关键字很不一样,它能够锁住文件中的某一部分,非常的强悍!用了这个方法我们能够精确锁定住我们需要锁住的文件的部分内容

  • void Unlock (long position,long length):正好和lock方法相反,对于文件部分的解锁

MemoryStream

创建一个流,其后备存储为内存。

使用无符号字节数组创建的内存流提供不可调整大小的数据流。 使用字节数组时,既不能追加流,也不能收缩流,不过,根据传递到构造函数的参数,可以修改现有内容。 空内存流可调整大小,可以写入和读取。

由于MemoryStream是通过无符号字节数组组成的,可以说MemoryStream的性能可以算比较出色,所以它担当起了一些其他流进行数据交换时的中间工作,同时可降低应用程序中对临时缓冲区和临时文件的需要.

构造函数

  • MemoryStream():使用初始化为零的可扩展容量初始化 MemoryStream 类的新实例。

使用初始化为零的可扩展容量初始化 MemoryStream 类的新实例。

CanSeek属性CanRead和CanWrite属性都设置为 true。

使用 SetLength 该方法将长度设置为大于当前流的容量的值时,当前流的容量会自动增加。

此构造函数公开返回的基础流 GetBuffer 。

  • MemoryStream(Byte[]):基于指定的字节数组初始化 MemoryStream 类的无法调整大小的新实例。

基于指定的字节数组初始化 MemoryStream 类的无法调整大小的新实例。

CanSeek属性CanRead和CanWrite属性都设置为 true。 Capacity 设置为指定字节数组的长度。 可将新流写入,但不可调整大小。

流的长度不能设置为大于指定字节数组的初始长度的值;但是, (可以看到 SetLength) 截断流。

此构造函数不公开基础流。 GetBuffer throws UnauthorizedAccessException.

  • MemoryStream(Int32):使用按指定要求初始化的可扩展容量初始化 MemoryStream 类的新实例。

使用按指定要求初始化的可扩展容量初始化 MemoryStream 类的新实例。

CanSeek属性CanRead和CanWrite属性都设置为 true。

使用 SetLength 此方法将长度设置为大于当前流的容量的值时,容量会自动增加。

此构造函数公开返回的基础流 GetBuffer

  • MemoryStream(Byte[], Int32, Int32, Boolean, Boolean):在 MemoryStream 属性和调用 CanWrite 的能力按指定设置的状态下,基于字节数组的指定区域初始化 GetBuffer() 类的新实例。

和CanRead、CanSeek属性都设置为true。 将 Capacity 设置为 count。

可以将新流实例写入其中,但 Capacity 基础字节数组无法更改。 流的长度不能设置为大于指定字节数组的初始长度的值;

方法

  • virtual byte[] GetBuffer(): 返回从中创建此流的无符号字节的数组

请注意,缓冲区包含可能未使用的已分配字节。 例如,如果将字符串“test”写入 MemoryStream 对象,则返回 GetBuffer 的缓冲区长度为 256,而不是 4,未使用 252 个字节。 若要仅获取缓冲区中的数据,请使用 ToArray 该方法;但是, ToArray 会在内存中创建数据的副本。

这个方法使用时需要小心,因为这个方法返回无符号字节数组,也就是说,即使我只输入几个字符例如”HellowWorld”我们只希望返回11个数据就行,可是这个方法会把整个缓冲区的数据,包括那些已经分配但是实际上没有用到的字节数据都返回出来;

  • virtual void WriteTo(Stream stream): 将此内存流的整个内容写入到另一个流中。

memoryStream常用起中间流的作用,所以在处理完后将内存流写入其他流中;

BufferedStream

将缓冲层添加到另一个流上的读取和写入操作。
BufferedStream能够实现流的缓存,换句话说也就是在内存中能够缓存一定的数据而不是

时时给系统带来负担,同时BufferedStream可以对缓存中的数据进行写入或是读取,所以对流的性能带来一定的提升,但是无法同时进行读取或写入工作,如果不使用缓冲区也行,BufferedStream能够保证不用缓冲区时不会降低因缓冲区带来的读取或写入性能的下降。

为什么MemoryStream 同样也是在内存中对流进行操作,和BufferedStream有什么区别呢?BufferedStream并不是将所有内容都存放到内存中,而MemoryStream则是。BufferedStream必须跟其他流如FileStream结合使用,而MemoryStream则不用,聪明的你肯定能够想到,BufferedStream必然类似于一个流的包装类,对流进行”缓存功能的扩展包装”,所以BufferedStream的优势不仅体现在其原有的缓存功能上,更体现在如何帮助原有类实现其功能的扩展层面上。

NetworkStream

如果服务器和客户端之间基于TCP连接的,他们之间能够依靠一个稳定的字节流进行相互传输信息,这也是

NetworkStream的最关键的作用,有了这个神奇的协议,NetWorkStream便能向其他流一样在网络中(进行点对点的传输)

  • NetworkStream只能用在具有Tcp/IP协议之中,如果用在UDP中编译不报错,会报异常

  • NetworkStream 是面向连接的

  • 在网络中利用流的形式传递信息

  • 必须借助Socket (也称之为流式socket),或使用一些返回的返回值,例如TcpClient类的GetStream方法

  • 用法和普通流方法几乎一模一样,但具有特殊性

可以在类实例 NetworkStream 上同时执行读取和写入操作,而无需同步。 只要写入操作有一个唯一线程,读取操作有一个唯一线程,读取和写入线程之间就不会有交叉干扰,也不需要同步。

常用读取方式

1
2
3
4
5
6
7
8
9
public override void OnActionExecuting(ActionExecutingContext context)
{
//在ASP.NET Core中Request Body是Stream的形式
StreamReader stream = new StreamReader(context.HttpContext.Request.Body);
string body = stream.ReadToEnd();
_logger.LogDebug("body content:" + body);
base.OnActionExecuting(context);
}

直接报一个这个错System.InvalidOperationException: Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead.大致的意思就是同步操作不被允许,请使用ReadAsync的方式或设置AllowSynchronousIO为true。

同步读取

如何设置AllowSynchronousIO的值。第一种方式是在ConfigureServices中配置,操作如下

1
2
3
4
services.Configure<KestrelServerOptions>(options =>
{
options.AllowSynchronousIO = true;
});

还有一种方式,可以不用在ConfigureServices中设置,通过IHttpBodyControlFeature的方式设置,具体如下

1
2
3
4
5
6
7
8
9
10
11
12
public override void OnActionExecuting(ActionExecutingContext context)
{
var syncIOFeature = context.HttpContext.Features.Get<IHttpBodyControlFeature>();
if (syncIOFeature != null)
{
syncIOFeature.AllowSynchronousIO = true;
}
StreamReader stream = new StreamReader(context.HttpContext.Request.Body);
string body = stream.ReadToEnd();
_logger.LogDebug("body content:" + body);
base.OnActionExecuting(context);
}

这种方式同样有效,通过这种方式操作,不需要每次读取Body的时候都去设置,只要在准备读取Body之前设置一次即可。这两种方式都是去设置AllowSynchronousIO为true,但是我们需要思考一点,微软为何设置AllowSynchronousIO默认为false,说明微软并不希望我们去同步读取Body。通过查找资料得出了这么一个结论

Kestrel:默认情况下禁用 AllowSynchronousIO(同步IO),线程不足会导致应用崩溃,而同步I/O API(例如HttpRequest.Body.Read)是导致线程不足的常见原因。

异步读取

通过上面我们了解到微软并不希望我们通过设置AllowSynchronousIO的方式去操作,因为会影响性能。那我们可以使用异步的方式去读取,这里所说的异步方式其实就是使用Stream自带的异步方法去读取,如下所示

1
2
3
4
5
6
7
public override void OnActionExecuting(ActionExecutingContext context)
{
StreamReader stream = new StreamReader(context.HttpContext.Request.Body);
string body = stream.ReadToEndAsync().GetAwaiter().GetResult();
_logger.LogDebug("body content:" + body);
base.OnActionExecuting(context);
}

ASP.NET Core中许多操作都是异步操作,甚至是过滤器或中间件都可以直接返回Task类型的方法,因此我们可以直接使用异步操作

1
2
3
4
5
6
7
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
StreamReader stream = new StreamReader(context.HttpContext.Request.Body);
string body = await stream.ReadToEndAsync();
_logger.LogDebug("body content:" + body);
await next();
}

这两种方式的操作优点是不需要额外设置别的,只是通过异步方法读取即可,也是我们比较推荐的做法。

重复读取

上面我们演示了使用同步方式和异步方式读取RequestBody,但是这样真的就可以了吗?其实并不行,这种方式每次请求只能读取一次正确的Body结果,如果继续对RequestBody这个Stream进行读取,将读取不到任何内容,首先来举个例子

1
2
3
4
5
6
7
8
9
10
11
12
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
StreamReader stream = new StreamReader(context.HttpContext.Request.Body);
string body = await stream.ReadToEndAsync();
_logger.LogDebug("body content:" + body);

StreamReader stream2 = new StreamReader(context.HttpContext.Request.Body);
string body2 = await stream2.ReadToEndAsync();
_logger.LogDebug("body2 content:" + body2);

await next();
}

上面的例子中body里有正确的RequestBody的结果,但是body2中是空字符串。

那到底该如何解决呢?也很简单,微软知道自己刨下了坑,自然给我们提供了解决办法,用起来也很简单就是加EnableBuffering

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
//操作Request.Body之前加上EnableBuffering即可
context.HttpContext.Request.EnableBuffering();

StreamReader stream = new StreamReader(context.HttpContext.Request.Body);
string body = await stream.ReadToEndAsync();
_logger.LogDebug("body content:" + body);

context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);
StreamReader stream2 = new StreamReader(context.HttpContext.Request.Body);
//注意这里!!!我已经使用了同步读取的方式
string body2 = stream2.ReadToEnd();
context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);
_logger.LogDebug("body2 content:" + body2);

await next();
}

通过添加Request.EnableBuffering()我们就可以重复的读取RequestBody了,看名字我们可以大概的猜出来,他是和缓存RequestBody有关,需要注意的是Request.EnableBuffering()要加在准备读取RequestBody之前才有效果,否则将无效,而且每次请求只需要添加一次即可。而且大家看到了我第二次读取Body的时候使用了同步的方式去读取的RequestBody,是不是很神奇,待会的时候我们会从源码的角度分析这个问题。

源码探究

上面我们看到了通过StreamReader的ReadToEnd同步读取Request.Body需要设置AllowSynchronousIO为true才能操作,但是使用StreamReader的ReadToEndAsync方法却可以直接操作。

StreamReader和Stream的关系

我们看到了都是通过操作StreamReader的方法即可,那关我Request.Body啥事,别急咱们先看一看这里的操作,首先来大致看下ReadToEnd的实现了解一下StreamReader到底和Stream有啥关联,找到ReadToEnd方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public override string ReadToEnd()
{
ThrowIfDisposed();
CheckAsyncTaskInProgress();
// 调用ReadBuffer,然后从charBuffer中提取数据。
StringBuilder sb = new StringBuilder(_charLen - _charPos);
do
{
//循环拼接读取内容
sb.Append(_charBuffer, _charPos, _charLen - _charPos);
_charPos = _charLen;
//读取buffer,这是核心操作
ReadBuffer();
} while (_charLen > 0);
//返回读取内容
return sb.ToString();
}

通过这段源码我们了解到了这么个信息,一个是StreamReader的ReadToEnd其实本质是通过循环读取ReadBuffer然后通过StringBuilder去拼接读取的内容,核心是读取ReadBuffer方法,由于代码比较多,我们找到大致呈现一下核心操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (_checkPreamble)
{
//通过这里我们可以知道本质就是使用要读取的Stream里的Read方法
int len = _stream.Read(_byteBuffer, _bytePos, _byteBuffer.Length - _bytePos);
if (len == 0)
{
if (_byteLen > 0)
{
_charLen += _decoder.GetChars(_byteBuffer, 0, _byteLen, _charBuffer, _charLen);
_bytePos = _byteLen = 0;
}
return _charLen;
}
_byteLen += len;
}
else
{
//通过这里我们可以知道本质就是使用要读取的Stream里的Read方法
_byteLen = _stream.Read(_byteBuffer, 0, _byteBuffer.Length);
if (_byteLen == 0)
{
return _charLen;
}
}

通过上面的代码我们可以了解到StreamReader其实是工具类,只是封装了对Stream的原始操作,简化我们的代码ReadToEnd方法本质是读取Stream的Read方法。接下来我们看一下ReadToEndAsync方法的具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public override Task<string> ReadToEndAsync()
{
if (GetType() != typeof(StreamReader))
{
return base.ReadToEndAsync();
}
ThrowIfDisposed();
CheckAsyncTaskInProgress();
//本质是ReadToEndAsyncInternal方法
Task<string> task = ReadToEndAsyncInternal();
_asyncReadTask = task;

return task;
}

private async Task<string> ReadToEndAsyncInternal()
{
//也是循环拼接读取的内容
StringBuilder sb = new StringBuilder(_charLen - _charPos);
do
{
int tmpCharPos = _charPos;
sb.Append(_charBuffer, tmpCharPos, _charLen - tmpCharPos);
_charPos = _charLen;
//核心操作是ReadBufferAsync方法
await ReadBufferAsync(CancellationToken.None).ConfigureAwait(false);
} while (_charLen > 0);
return sb.ToString();
}

通过这个我们可以看到核心操作是ReadBufferAsync方法,代码比较多我们同样看一下核心实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
byte[] tmpByteBuffer = _byteBuffer;
//Stream赋值给tmpStream
Stream tmpStream = _stream;
if (_checkPreamble)
{
int tmpBytePos = _bytePos;
//本质是调用Stream的ReadAsync方法
int len = await tmpStream.ReadAsync(new Memory<byte>(tmpByteBuffer, tmpBytePos, tmpByteBuffer.Length - tmpBytePos), cancellationToken).ConfigureAwait(false);
if (len == 0)
{
if (_byteLen > 0)
{
_charLen += _decoder.GetChars(tmpByteBuffer, 0, _byteLen, _charBuffer, _charLen);
_bytePos = 0; _byteLen = 0;
}
return _charLen;
}
_byteLen += len;
}
else
{
//本质是调用Stream的ReadAsync方法
_byteLen = await tmpStream.ReadAsync(new Memory<byte>(tmpByteBuffer), cancellationToken).ConfigureAwait(false);
if (_byteLen == 0)
{
return _charLen;
}
}

通过上面代码我可以了解到StreamReader的本质就是读取Stream的包装,核心方法还是来自Stream本身。我们之所以大致介绍了StreamReader类,就是为了给大家呈现出StreamReader和Stream的关系,否则怕大家误解这波操作是StreamReader的里的实现,而不是Request.Body的问题,其实并不是这样的所有的一切都是指向Stream的Request的Body就是Stream这个大家可以自己查看一下,了解到这一步我们就可以继续了。

HttpRequest的Body

上面我们说到了Request的Body本质就是Stream,Stream本身是抽象类,所以Request.Body是Stream的实现类。默认情况下Request.Body的是HttpRequestStream的实例,我们这里说了是默认,因为它是可以改变的,我们一会再说。我们从上面StreamReader的结论中得到ReadToEnd本质还是调用的Stream的Read方法,即这里的HttpRequestStream的Read方法,我们来看一下具体实现

1
2
3
4
5
6
7
8
9
10
public override int Read(byte[] buffer, int offset, int count)
{
//知道同步读取Body为啥报错了吧
if (!_bodyControl.AllowSynchronousIO)
{
throw new InvalidOperationException(CoreStrings.SynchronousReadsDisallowed);
}
//本质是调用ReadAsync
return ReadAsync(buffer, offset, count).GetAwaiter().GetResult();
}

通过这段代码我们就可以知道了为啥在不设置AllowSynchronousIO为true的情下读取Body会抛出异常了吧,这个是程序级别的控制,而且我们还了解到Read的本质还是在调用ReadAsync异步方法

1
2
3
4
public override ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken = default)
{
return ReadAsyncWrapper(destination, cancellationToken);
}

ReadAsync本身并无特殊限制,所以直接操作ReadAsync不会存在类似Read的异常。

通过这个我们得出了结论Request.Body即HttpRequestStream的同步读取Read会抛出异常,而异步读取ReadAsync并不会抛出异常只和HttpRequestStream的Read方法本身存在判断AllowSynchronousIO的值有关系。

EnableBuffering神奇的背后

我们在上面的示例中看到了,如果不添加EnableBuffering的话直接设置RequestBody的Position会报NotSupportedException这么一个错误,而且加了它之后我居然可以直接使用同步的方式去读取RequestBody,首先我们来看一下为啥会报错,我们从上面的错误了解到错误来自于HttpRequestStream这个类,上面我们也说了这个类继承了Stream抽象类,通过源码我们可以看到如下相关代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//不能使用Seek操作
public override bool CanSeek => false;
//允许读
public override bool CanRead => true;
//不允许写
public override bool CanWrite => false;
//不能获取长度
public override long Length => throw new NotSupportedException();
//不能读写Position
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
//不能使用Seek方法
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}

相信通过这些我们可以清楚的看到针对HttpRequestStream的设置或者写相关的操作是不被允许的,这也是为啥我们上面直接通过Seek设置Position的时候为啥会报错,还有一些其他操作的限制,总之默认是不希望我们对HttpRequestStream做过多的操作,特别是设置或者写相关的操作。但是我们使用EnableBuffering的时候却没有这些问题,究竟是为什么?接下来我们要揭开它的什么面纱了。首先我们从Request.EnableBuffering()这个方法入手

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

/// <summary>
/// 确保Request.Body可以被多次读取
/// </summary>
/// <param name="request"></param>
public static void EnableBuffering(this HttpRequest request)
{
BufferingHelper.EnableRewind(request);
}


//默认内存中可缓存的大小为30K,超过这个大小将会被存储到磁盘
internal const int DefaultBufferThreshold = 1024 * 30;

/// <summary>
/// 这个方法也是HttpRequest扩展方法
/// </summary>
/// <returns></returns>
public static HttpRequest EnableRewind(this HttpRequest request, int bufferThreshold = DefaultBufferThreshold, long? bufferLimit = null)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
//先获取Request Body
var body = request.Body;
//默认情况Body是HttpRequestStream这个类CanSeek是false所以肯定会执行到if逻辑里面
if (!body.CanSeek)
{
//实例化了FileBufferingReadStream这个类,看来这是关键所在
var fileStream = new FileBufferingReadStream(body, bufferThreshold,bufferLimit,AspNetCoreTempDirectory.TempDirectoryFactory);
//赋值给Body,也就是说开启了EnableBuffering之后Request.Body类型将会是FileBufferingReadStream
request.Body = fileStream;
//这里要把fileStream注册给Response便于释放
request.HttpContext.Response.RegisterForDispose(fileStream);
}
return request;
}

从上面这段源码实现中我们可以大致得到两个结论

  • BufferingHelper的EnableRewind方法也是HttpRequest的扩展方法,可以直接通过Request.EnableRewind的形式调用,效果等同于调用Request.EnableBuffering因为EnableBuffering也是调用的EnableRewind
  • 启用了EnableBuffering这个操作之后实际上会使用FileBufferingReadStream替换掉默认的HttpRequestStream,所以后续处理RequestBody的操作将会是FileBufferingReadStream实例

通过上面的分析我们也清楚的看到了,核心操作在于FileBufferingReadStream这个类,而且从名字也能看出来它肯定是也继承了Stream抽象类

总结

本篇文章篇幅比较多,如果你想深入的研究相关逻辑,希望本文能给你带来一些阅读源码的指导。为了防止大家深入文章当中而忘记了具体的流程逻辑,在这里我们就大致的总结一下关于正确读取RequestBody的全部结论

  • 首先关于同步读取Request.Body由于默认的RequestBody的实现是HttpRequestStream,但是HttpRequestStream在重写Read方法的时候会判断是否开启AllowSynchronousIO,如果未开启则直接抛出异常。但是HttpRequestStream的ReadAsync方法并无这种限制,所以使用异步方式的读取RequestBody并无异常。
  • 虽然通过设置AllowSynchronousIO或使用ReadAsync的方式我们可以读取RequestBody,但是RequestBody无法重复读取,这是因为HttpRequestStream的Position和Seek都是不允许进行修改操作的,设置了会直接抛出异常。为了可以重复读取,我们引入了Request的扩展方法EnableBuffering通过这个方法我们可以重置读取位置来实现RequestBody的重复读取。
  • 关于开启EnableBuffering方法每次请求设置一次即可,即在准备读取RequestBody之前设置。其本质其实是使用FileBufferingReadStream代替默认RequestBody的默认类型HttpRequestStream,这样我们在一次Http请求中操作Body的时候其实是操作FileBufferingReadStream,这个类重写Stream的时候Position和Seek都是可以设置的,这样我们就实现了重复读取。
  • FileBufferingReadStream带给我们的不仅仅是可重复读取,还增加了对RequestBody的缓存功能,使得我们在一次请求中重复读取RequestBody的时候可以在Buffer里直接获取缓存内容而Buffer本身是一个MemoryStream。当然我们也可以自己实现一套逻辑来替换Body,只要我们重写的时候让这个Stream支持重置读取位置即可。

摘抄 yi念之间 原文地址