2023年4月

题目链接:
https://ac.nowcoder.com/acm/contest/54484/B

题意很简单,但是数据范围偏大。

错排公式

首先来推导一下错排公式:

\[D(n) = n!\sum_{k=0}^{n}\frac{(-1)^k}{k!}
\]

设一个函数:

\[S_i表示一个排列中p_i = i的方案数
\]

那么我们可以知道:

\[D(n) = n! - |\cup_{i=1}^{n}S_i|
\]

这个表示
所有方案数
减去
至少有一个位置放对的方案数

现在来考虑一下如何处理后面这个并集,并集往往是不好求的,而
交集会好求很多
,所以在求并集的时候我们往往采取容斥原理将一个并集
转换成诸多交集的加减运算

我们用一个图可以来表示当
n = 3
的情况:

其中有:

\[|S_1 \cup S_2 \cup S_3| = |S_1| + |S_2| + |S_3| - |S_1 \cap S_2| - |S_1 \cap S_3| - |S_2 \cap S_3| + |S_1 \cap S2 \cap S_3|
\]

扩展一下就可以得到下面的柿子:

\[|\cup_{i=1}^{n}S_i| = \sum_{k=1}^{n}(-1)^k\sum_{1\leq i_1 \leq i_2 \leq ... \leq i_k \leq n}|S_{i1}\cap S_{i2} ... \cap S_{ik}|
\]

然后有:

\[\sum_{1\leq i_1 \leq i_2 \leq ... \leq i_k \leq n}|S_{i1}\cap S_{i2} ... \cap S_{ik}| = C_{n}^{k}(n-k)!
\]

这个表示啥呢,左边这个柿子的含义其实是
i1 ~ ik
都放对了,其他位置上无所谓的方案数,就等同于在
n
个位置中选择
k
个放对,剩下的随便放的方案数。

所以可得下面的柿子:

\[|\cup_{i=1}^{n}S_i| = \sum_{k=1}^{n}(-1)^kC_{n}^{k}(n-k)!
\]

然后化简得:

\[|\cup_{i=1}^{n}S_i| = \sum_{k=1}^{n}\frac{(-1)^k n!}{k!}
\]

然后代回到一开始的答案表达式中:

\[D(n) = n! - \sum_{k=1}^{n}\frac{(-1)^k n!}{k!}
\]


n!
提出来,再化简一下得到:

\[D(n) = n! \sum_{k=0}^{n}\frac{(-1)^k}{k!}
\]

回到本题

但是有这个柿子依然不好写这题,这题如果是
1e7
就可以直接O(n)写了,但是这题是
1e9
的数据范围,可以考虑一下分段打表(一般
要求函数可以递推
),但是这个表达式好像不是很好打,我们来分析一下。

首先网上有一个比较有名递推式(证明略):

\[D(n) = (n-1)[D(n - 1) + D(n - 2)]
\]

这个递推需要用到前两项,也就是说我们需要打两个表,然后才可以做,有点麻烦,但是其实是可以只用一项的。

我看网路上都没有用下面这种方式递推的,我在这里写一下。

我们考虑
D(n) -> D(n + 1)
这样的转移:

\[D(n) = n! \sum_{k=0}^{n}\frac{(-1)^k}{k!}
\]

\[D(n + 1) = (n + 1)! \sum_{k=0}^{n + 1}\frac{(-1)^k}{k!}
\newline = (n + 1)![\sum_{k=0}^{n}\frac{(-1)^k}{k!} + \frac{(-1)^{n + 1}}{(n + 1)!}]
\newline = (n + 1)!\sum_{k=0}^{n}\frac{(-1)^k}{k!} + (-1)^{n + 1}
\newline = (n + 1) \times n!\sum_{k=0}^{n}\frac{(-1)^k}{k!} + (-1)^{n + 1}
\newline = (n+1) \times D(n) + (-1)^{n+1}\]

然后令段大小
T = 1e7
打表打出
D(0), D(T), D(2T) ... D(100T)
就好了。

最终的复杂度是
O(n)
但是常数极小,所以可以过。

Code:

#include <bits/stdc++.h>
#define int long long
using namespace std;

const int p = 1e9 + 7, T = 1e7;

int a[110] =
{
1,824182295,933713113,474482547,930651136,251064654,637937211,229643390,307311871,448853213,
322273426,398890147,194914852,884947442,154199209,881788023,389699639,733217502,601739182,
372305477,213823357,713959988,498202615,196342945,324300550,154001751,974475946,540773759,
467881322,257531902,598680559,367927849,971346692,94577421,617166666662,128327758,503709458,
253566817,820144401,13965056,82358069,805941568,533047638,69430220,686678173,297170813,
34546238,323435423,499126069,487532712,468899710,790590914,581347156,955359050,700529992,
518280890,98592091,64544225,988209678,422603955,40661679,174468756,573631136,757666666557,
710709955,775098981,499158883,969149294,880429710,42564126,333697951,522067888,579797877,
528967798,717694718,309384913,31308092,316850320,220854491,878646494,963974981,377654637,
705101053,542246848,466289530,750036412,819636314,688721174,464087273,517164631,256789690,
482685016,276682441,473333947,340221393,762927538,624766601,984537252,977632075,34192646,
402182971,977005016
};

int mo(int x){return (x % p + p) % p;}

void solve()
{
	int n;cin >> n;
	int ans = a[n / T];
	for(int i = n / T * T + 1;i <= n; ++ i)ans = mo(ans * i % p + ((i & 1) ? -1 : 1));
	cout << ans << '\n';
}


void table()
{
	int x = 1;//d(0) = 1,这个有点特殊
    cout << x << ",";
    int cnt = 1;
    for(int i = 1;i <= 1e9; ++ i)
    {
        x = x * i % p;
        if(i & 1)x = (x - 1 + p) % p;
        else x = (x + 1) % p;
        
        if(i % T == 0)
        {
        	cout << x << ",";
    		cnt ++;
    	}
    	
        if(cnt % 10 == 0)
        {
        	cout << '\n';
        	cnt = 1;
        }
        
    }
}

signed main()
{
    table();
	solve();
	//return 0;
}

前言

之前一篇文章介绍了 日志库zap
https://www.cnblogs.com/zichliang/p/17311480.html
毋庸置疑,zap库无论是Golang在项目中 还是生产中都极其优秀的一个数据库,而且他是当今Go最快的日志库 性能最高的日志库。
但是今天缺不是来说他的,今天介绍的是另一款日志库 logrus
虽然 logrus 已经不维护 且不更新,但是个人感觉logrus比zap 要好用很多,不是说性能,是使用的简易程度而言。

logrus介绍

首先贴上github 地址:

https://github.com/sirupsen/logrus

logrus相比较zap 没有完整的日志库级别 但是比起自带的logger还是要丰富很多
一共有七种日志库级别 Trace, Debug, Info, Warning, Error, Fatal, Panic。

性能:相别zap 略逊一筹

结构化而言:日期时间,级别,信息

而他的优缺点则更为明显:

  • 优点在一开始也提及了 就是使用非常简单。
  • 缺点也更加显而易见 性能一般,日志库等级不丰富(其实七个日库等级 一般而言也够用。)

安装

go get -u github.com/sirupsen/logrus

简单使用

package test

import (
	"github.com/sirupsen/logrus"
	"os"
	"testing"
)

func init() {
	logrus.SetReportCaller(false)

	logrus.SetFormatter(&logrus.JSONFormatter{})

	logrus.SetOutput(os.Stdout)

	logrus.SetLevel(logrus.WarnLevel)
}

func TestLog(t *testing.T) {
	//time="2023-04-17T11:06:36+08:00" level=info msg="hello,world" 用户=  "创建时的日志"

	// WithFields从标准记录器创建一个条目,并向其添加多个字段。这只是一个' WithField '的助手,为每个字段调用它一次。注意,它不会记录,直到你调用调试,打印,信息,警告,致命或恐慌的条目返回。
	logger := logrus.WithFields(logrus.Fields{
		"用户": "创建时的日志",
	})
	logger.Info("hello,world")
}

WithFields

WithFields从标准记录器创建一个条目,并向其添加多个字段。
这只是一个' WithField '的助手,为每个字段调用它一次。注意,它不会记录,直到你调用调试,打印,信息,警告,等条目返回。
可以随意调用

logger := logrus.WithFields(logrus.Fields{
	"用户": "创建时的日志",
})
logger.Trace("Trace 级别的信息")
logger.Info("Info 级别的信息")
logger.Debug("Debug 级别的信息")

logrus.Warn("Warn 级别的信息")
logrus.Error("Error 级别的信息")
logrus.Fatal("Fatal 级别的信息")
logrus.Panic("Panic 级别的信息") // 会报错

SetReportCaller

至于 logrus.SetReportCaller(true)
我们直接看结果

logrus.SetReportCaller(true)
// time="2023-04-17T11:06:52+08:00" level=info msg="hello,world" func=DoubleCarbon/test.TestLog file="E:/Golang/DoubleCarbon/test/log_test.go:16" 用 户="创建时的日志"
logrus.SetReportCaller(false)
// time="2023-04-17T11:06:36+08:00" level=info msg="hello,world" 用户=  "创建时的日志"

SetFormatter

SetFormatter设置标准记录器格式化程序。
以 JSON 格式而不是默认的 ASCII 格式化程序记录。

SetOutput

输出到标准输出而不是默认标准输出
可以是任何io编写器
这里我写的输出到 终端中 也可以输出到文件中

SetLevel

仅记录警告严重性或更高级别。
可以任意定义。一般都是定义debug 以上级别的
不然日志太多就没有意义了。

Hooks

https://github.com/sirupsen/logrus/wiki/Hooks
使用上面这些库 包含了可以所有可以连接logrus的hooks

举例

因为一般来说要发送日志 肯定不可能选择 MySQL这种数据库 ,因为日志运行起来量是很大的,所以一般来说
会选择 es 或者redis 或者mongodb 这种非关系数据库。
这里就简单举个redis的例子

安装

logrus-redis-hook

go get github.com/rogierlommers/logrus-redis-hook

简易使用

package main

import (
	logredis "github.com/rogierlommers/logrus-redis-hook"
	"io/ioutil"

	"github.com/sirupsen/logrus"
)

func init() {
	hookConfig := logredis.HookConfig{
		Host:     "localhost",
		Key:      "my_redis_key",
		Format:   "v0",
		App:      "my_app_name",
		Port:     6379,
		Hostname: "my_app_hostname", // will be sent to field @source_host
		DB:       0,                 // optional
		TTL:      3600,
		Password: "*****", // 写你的密码
	}

	hook, err := logredis.NewHook(hookConfig)
	if err == nil {
		logrus.AddHook(hook)
	} else {
		logrus.Errorf("logredis error: %q", err)
	}
}

func main() {
	// when hook is injected succesfully, logs will be sent to redis server
	logrus.Info("just some info logging...")

	// we also support log.WithFields()
	logrus.WithFields(logrus.Fields{
		"animal": "walrus",
		"foo":    "bar",
		"this":   "that"}).
		Info("additional fields are being logged as well")

	// If you want to disable writing to stdout, use setOutput
	logrus.SetOutput(ioutil.Discard)
	logrus.Info("This will only be sent to Redis")
}

如图所示

直接就在redis里写入了日志信息

如果失败 如下图所示

gin框架使用logrus

直接看官方文档把

// a gin with logrus demo

var log = logrus.New()

func init() {
	// Log as JSON instead of the default ASCII formatter.
	log.Formatter = &logrus.JSONFormatter{}
	// Output to stdout instead of the default stderr
	// Can be any io.Writer, see below for File example
	f, _ := os.Create("./gin.log")
	log.Out = f
	gin.SetMode(gin.ReleaseMode)
	gin.DefaultWriter = log.Out
	// Only log the warning severity or above.
	log.Level = logrus.InfoLevel
}

func main() {
	// 创建一个默认的路由引擎
	r := gin.Default()
	// GET:请求方式;/hello:请求的路径
	// 当客户端以GET方法请求/hello路径时,会执行后面的匿名函数
	r.GET("/hello", func(c *gin.Context) {
		log.WithFields(logrus.Fields{
			"animal": "walrus",
			"size":   10,
		}).Warn("A group of walrus emerges from the ocean")
		// c.JSON:返回JSON格式的数据
		c.JSON(200, gin.H{
			"message": "Hello world!",
		})
	})
	// 启动HTTP服务,默认在0.0.0.0:8080启动服务
	r.Run()
}

重新学习这两个API的起因

在本周五有线上的项目,16:30开始验证线上环境。
开始都是顺顺利利,一帆风顺。
大概17:50左右,我正在收拾东西。
准备下班去王者峡谷骑着我的船溜达一圈。
可是天降意外,给我派了一个bug。
测试给我说:有一条数据的详情页有数据但是在页面中没有显示数据。
不可能,绝对不可能。当时我信誓旦旦的。蛮自信。
当时怀疑是这条数据本身就没有详细数据。用户还没有补充详情。
但是测试给我发了一张图片。
我看见控制台出现红色的 Uncaught SyntaxError
映入我的眼球,感觉就像在向我宣战:
此时我虚了,感觉十有八九就是一个bug。
后来经过排查,发现是 JSON.string()引起的。
故而,今天周六简单记录一下 JSON.string它并不是我们想的那样简单。

大家对 JSON.string() 的第一印象是什么?

我现在依稀记得:
JSON.stringify() 方法将一个 JavaScript 对象或值转换为 JSON 字符串。
其他的就没有什么特别的印象。
其实,它在对不同类型数据处理时,会做出不同的反应。
下面坐好我在峡谷买的船,一起来看一下。

JSON.string()转换的值中有 toJSON() 方法,那么返回值直接替代当前这个对象

var obj = {
  name:'小魔神',
  like:'喜欢和乌鸦说话',
  toJSON: function () {
      return '活下去';
  }
};
var newStr = JSON.stringify(obj);
console.log(newStr);
此时,你认为输出的值是什么?
认真考虑10s。是什么?
最后会输出  '活下去'
是不是很意外,是不是很惊喜。竟然是这个结果。
这的是我们都没有想到对吧?
子所以这这个结果:
因为:obj这个对象中有  toJSON()方法。
那么这个方法的返回值将会替代当前这个对象。所以是 '活下去'

有 toJSON() 方法没有返回值会怎么样?

有的小伙伴这个时候就在想了。
你说的是因为转换中有 toJSON()方法并且有返回值(retuen)才会替代当前的对象。
如果有 toJSON()方法但是没有返回值是不是就不会替换当前这个对象了呢?
于是我们写下了这样的代码
let obj = {
  name: '小魔神',
  like: '喜欢和乌鸦说话',
  toJSON: function() {
    console.log('我没有返回值')
  }
};
let newStr = JSON.stringify(obj);
console.log(newStr);

大家觉得是输出什么结果?
思考5s钟,你觉得是啥?
输出 undefined。为什么是undefined呢?
因为函数没有返回值的时候,默认返回 undefined
也就是说:
toJSON: function() {
  return undefined
  console.log('我没有返回值')
}
你以为 JSON.stringify 的神奇之处只有这点。
那你就错了,它有很多我们之前可能没有了解的地方。
我们接着往下看,看看还有什么什么黑魔法

无法序列化错误对象,错误对象将会被转为为空对象

// 创建了一个错误对象
const err = new Error('错的不是我,而是这个世道。')
let obj = {
		name:'小魔神',
		like:'喜欢和乌鸦说话',
		err
}; 
console.log(JSON.stringify(obj));
// 我们发现 err 这个错误对象变为了空对象 {}
是不是觉得 JSON.stringify 有点东西在里面了
我们继续往下看

对象中不可枚举的值将不会对其序列化

let obj = {
	name:'小魔神',
	like:'喜欢和乌鸦说话',
}; 
Object.defineProperty(obj, 'name', {
	value: '魔神',
	enumerable: false // 将它设置为不可枚举
})
// name属性将不会被输出。[因为不会对它进行器序列化]
console.log(JSON.stringify(obj)); 
---这里可以写一
是不是觉得 JSON.stringify 越来越有意思了。

NaN 和 Infinity 及 null 都会被当做 null

// 1.7976931348623157E+10308 是浮点数的最大上限值 显示为Infinity
// -1.7976931348623157E+10308 是浮点数的最小下限值 显示为-Infinity
const obj = {
    infinityMax: 1.7976931348623157E+10308,
    infinityMin: -1.7976931348623157E+10308,
		a: NaN
}
console.log('obj输出的值是:', JSON.stringify(obj)); 

日期对象将会对其序列化为字符串string

const obj = {
    dateTime: new Date(),
    name: '小魔神',
    like: '喜欢和乌鸦说话',
}
const objCopy = JSON.parse(JSON.stringify(obj));
// 发现类型是字符串
console.log('类型是', typeof objCopy.dateTime)
// 因为是字符串就无法调用原来日期的getTime时间戳了
console.log(objCopy.dateTime.getTime())

所以在序列化日期对象的时候千千万万要注意。
因为它会将日期对象最后变成字符串。
从而导致之前的日期方法不能够调用。

循环引用的对象将会抛出错误

const obj = {
	name:'小魔神',
	like:'喜欢和乌鸦说话',
	sex:null
}
obj.sex = obj; //我们这里循环引用了,将会报错
const objCopy = JSON.parse(JSON.stringify(obj));
console.log("objCopy", objCopy)

undefined、函数、symbol值 在不同的场合将会发生不同的反应

undefined、函数[方法]、symbol值在不同的场合,
将会发生不同的''化学反应'。
在对象中,作为Value值的时候,在序列的时候将会忽略。
在对象中,将会被转化为null。
单独转化时,将会变为undefined。

undefined、函数、symbol值,在序列化过程中会被忽略 【出现在非数组对象的属性值中时】

let person = Symbol('小魔神');
const obj = {
	person,
	un: undefined,
	funSy: () => { console.log('前端已死') }
}
const objCopy = JSON.parse(JSON.stringify(obj));
console.log("objCopy",objCopy)
我们发现  undefined、函数、symbol值,在序列化过程中会被忽略

undefined、任意的函数、symbol 值将会换成 null(出现在数组中时)

let person = Symbol('小魔神');
let sayFun = function () { console.log("我太难了") }
let arr =[ undefined, person, sayFun]
const objCopy = JSON.parse(JSON.stringify(arr));
console.log("objCopy",objCopy)

所以在进行拷贝的时候,需要特别注意一下。
方法[任意的函数]会被丢失。不能调用

函数、undefined,symbol 被单独转换时,会返回 undefined

let a1 = JSON.stringify(function() {})
let a2 = JSON.stringify(undefined)
let a3 = JSON.stringify(Symbol('小魔神'))
console.log(a1)
console.log(a2)
console.log(a3)

遨游一圈的感想

我们平时在开发中,更多的是使用JSON.string()和JSON.parse()。
对我们需要的数据进行拷贝。
在拷贝的过程中需要注意以上的情况。
否者可能出现翻车。
JSON.string()也单独用在 get 请求将数组进行序列化。
这个时候各位小伙伴也需要注意一下。避免一些值丢失或者发生变化
还有就是将数据存储在localStorage、sessionStorage也会使用JSON.string()
我们也需要注意一下

使用JSON.string() 需要注意的点

1.使用JSON.string() 转换的值中,如果有 toJSON() 方法,那么返回值直接代替了当前的这个对象 
2.有 toJSON() 方法没有返回值会返回 undefined
3.无法序列化错误对象,错误对象将会被转为为空对象 
4.对象中不可枚举的值将不会对齐序列化 
5.NaN 和 Infinity 及 null 都会被当做 null。
6.日期对象将会对其序列化为字符串string
7.循环引用的对象将会抛出错误
8.undefined、任意的函数、symbol 值,在序列化过程中会被忽略【出现在非数组对象的属性值中时】
或者被转换成 null(出现在数组中时)。
函数、undefined,symbol 被单独转换时,会返回 undefined

简单说下 JSON.parse()

我们之前都在介绍 JSON.string(),我们现在简单说下 JSON.parse()。
毕竟他们俩是一对好基友
JSON.parse() 方法用于将一个 JSON 字符串转换为对象。
那什么是 JSON字符串呢?
JSON 是一种按照 JavaScript 对象语法的数据格式,这是 Douglas Crockford 推广的。
虽然它是基于 JavaScript 语法,但它独立于 JavaScript。
这也是为什么许多程序环境能够读取(解读)和生成 JSON。
JSON.parse(jsonStr,[function])
参数说明:
jsonStr:必需, 一个有效的 JSON 字符串。
function: 可选,一个转换结果的函数, 将为对象的每个成员调用此函数。

JSON需要注意的点事项

1.JSON 是一种纯数据格式,它只包含属性,没有方法。[或者说方法会被丢失]
也就是说:如果你原来的某一个对象中包含方法,在使用JSON之后,该方法会被丢失的哈~

2.JSON 数据格式为键/值对。 
JSON 要求在键值对 key 和 属性名称value周围使用双引号。单引号无效。
否者会报错的哈。Uncaught SyntaxError  未捕获的语法错误

3.JSON 可以将任何标准合法的 JSON 数据格式化保存,不只是数组和对象。
比如,一个单一的字符串或者数字或者一个空数组可以是合法的 JSON 对象。
这一点(第3点)很多人认为与第2点互相矛盾。
第二点不是说的是键值对key和value吗?
怎么单一的字符串和空数组,数字也可以呢?
其实没有矛盾,你直接使用 JSON.parse([])这样肯定是不行的。会出现语法错误
但是你先使用 JSON.stringify([]) 然后在使用JSON.parse就可以了

4.在使用 JSON.parse的使用需要注意第一个参数是否是JSON字符串。
否者会出现转化失败

键值对必须使用双引号进行包裹,否则就会报错

let jsonStr = "{'name': '张老师', 'sex':'男'}";
let newArr = JSON.parse(jsonStr)
console.log(newArr )
// 上面使用的是单引号,会报错

// 下面使用的是双引号--不会报错
// let jsonStr = '{"a1": "Hello", "b1": "World"}';
// let newArr = JSON.parse(jsonStr)
// console.log(newArr )

ps:键值对必须使用双引号进行包裹这里还隐含了另外一个意思
就是说 key和value必须要都要有双引号包裹。否则就会出现语法错误

使用 JSON.parse() 必须要符合JSON字符串

从上面的理解中,我们知道了使用JSON.parse() 必须要符合JSON字符串。
下面的使用 JSON.parse() 将会报错、
非常重要的点:使用JSON.parse() 必须要符合JSON字符串
非常重要的点:使用JSON.parse() 必须要符合JSON字符串
非常重要的点:使用JSON.parse() 必须要符合JSON字符串
重要的事情说三遍

直接转换数组

<script>
	let oldObj= []
	let arr = JSON.parse(oldObj)
	console.log('parse', parse )
</script>
将会报错 
Uncaught SyntaxError: Unexpected end of JSON input at JSON.parse (<anonymous>)
Uncaught SyntaxError  未捕获的语法错误
因为:使用JSON.parse() 必须要符合JSON字符串。
然而oldObj= [] 不是一个字符串

非要转换数组

let strJSON = JSON.stringify([])
let newObj = JSON.parse(strJSON)
console.log('newObj', newObj ) // 输出的是 []
我们先使用JSON.stringify([])将它转化为JSON字符串就可以了

JSON.parse() 不允许用逗号作为结尾

JSON.parse("[10, 20, 30, 40, ]");
JSON.parse('{"name1" : "澹台烬", }');

尾声

1.JSON 是一种纯数据格式,它只包含属性,没有方法。
2.JSON 要求在键值对 key 和 属性名称value周围使用双引号。单引号无效。
3.JSON 可以将任何标准合法的 JSON 数据格式化保存。
如:数组,对象,单一的字符串或者数字
4.JSON.parse() 不允许用逗号作为结尾
特别提醒:在使用 JSON.parse的使用需要注意第一个参数是否是JSON字符串。
如果你觉得我写的还不错:请我点一个推荐或者打赏。感谢各位大佬

简介

迭代器模式(Iterator Pattern),是一种结构型设计模式。给数据对象构建一套按顺序访问集合对象元素的方式,而不需要知道数据对象的底层表示。

迭代器模式是与集合共存的,我们只要实现一个集合,就需要同时提供这个集合的迭代器,就像Java中的Collection,List、Set、Map等,这些集合都有自己的迭代器。假如我们要实现一个这样的新的容器,就可以引入迭代器模式,给我们的容器实现一个迭代器。

作用

  1. 可以提供多种遍历对象的方式,把元素之间查找调用的责任交给迭代器,而不是聚合对象。
  2. 分离了集合对象的遍历行为,抽象出一个迭代器类来负责,这样既可以做到不暴露集合的内部结构,又可让外部代码透明地访问集合内部的数据。

实现步骤

  1. 创建迭代器接口,定义hasNext()和next()方法
  2. 创建数据容器接口,用来创建迭代器
  3. 创建具体数据列表,实现数据容器接口,可以创建迭代器,内含数据列表对象
  4. 创建某种数据对象的迭代器,实现hasNext()以及next()方法,并且关联上数据对象列表

UML

Java代码

迭代器抽象接口

 

//Iterator.java 迭代器抽象接口,提供next和hasNext方法
public interfaceIterator {public booleanhasNext();publicObject next();
}

具体迭代器

//ObjectIterator.java 对象迭代器,实现了抽象迭代器的方法,聚合了对象列表
public class ObjectIterator implementsIterator {privateObjectList objectList;intindex;publicObjectIterator(ObjectList objectList) {this.objectList =objectList;
}

@Override
public booleanhasNext() {if (index <objectList.size()) {return true;
}
return false;
}

@Override
publicObject next() {if (this.hasNext()) {//返回数据对象提供的get方法,每访问一次则增加下标 return objectList.get(index++);
}
return null;
}
}

数据容器接口

//Container.go 创建抽象容器接口,创建一个迭代器
public interfaceContainer {publicIterator createIterator();
}

具体数据对象

//ObjectList.java 对象列表,是一种数据容器,可以创建一个迭代器
public class ObjectList implementsContainer {private Object[] objects = { "Google", "Apple", "Amazon"};

@Override
publicIterator createIterator() {
System.out.println(
this.getClass().getName() + "::createIterator() [获取迭代器 ObjectIterator]");//把当前对象传给迭代器 return new ObjectIterator(this);
}
public voidsetObjects(Object[] objects) {this.objects =objects;
}
public intsize() {returnobjects.length;
}
public Object get(intindex) {returnobjects[index];
}
}

测试调用

    /** 迭代器模式是给数据容器创建单独的迭代器,用来遍历里面的数据对象
* 数据容器和迭代器相互关联,外部通过迭代器来访问数据容器
* 通过这种方式由迭代器类来负责数据遍历,这样可以做到不暴露集合的内部结构
*/ int i = 0;
ObjectList objectList
= newObjectList();
objectList.setObjects(
new String[] { "Thomas", "Merry", "Jack", "Tony", "Jerry", "Joey"});//for循环迭代对象 for (Iterator iter =objectList.createIterator(); iter.hasNext();) {
String name
=(String) iter.next();
System.out.println(
"objectList[" + i + "] = " +name);
i
++;
}
//while循环迭代对象 Iterator iter2 =objectList.createIterator();
objectList.setObjects(
new Integer[] { 3, 5, 7, 9, 11});while(iter2.hasNext()) {
System.out.println(iter2.next());
}

Go代码

迭代器抽象接口

//Iterator.go 迭代器抽象接口,提供next和hasNext方法
type Iterator interface{
HasNext()
boolNext()string}

具体迭代器

//ObjectIterator.go 对象迭代器,实现了抽象迭代器的方法,聚合了对象列表
type ObjectIterator struct{//迭代器索引
  index int
  //聚合了数据对象
  objectList *ObjectList
}
func (o *ObjectIterator) HasNext() bool{if o.index <o.objectList.Size() {return true}return false}func (o *ObjectIterator) Next() string{ifo.HasNext() {//返回数据对象提供的get方法,每访问一次下标增加1位 item :=o.objectList.Get(o.index)
o.index
+= 1 returnitem
}
return ""}

数据容器接口

//Container.go 创建抽象容器接口,创建一个迭代器
type Container interface{
CreateIterator() Iterator
}

具体数据对象

//ObjectList.go 对象列表,是一种数据容器,可以创建一个迭代器
type ObjectList struct{//内部的数据结构
  objects []string}func (o *ObjectList) CreateIterator() Iterator {
fmt.Println(
"ObjectList::CreateIterator() [获取迭代器 ObjectIterator]")//创建迭代器实例,绑定新建当前对象 return &ObjectIterator{
objectList: o,
}
}
func (o *ObjectList) SetObjects(objects []string) {
o.objects
=objects
}
func (o *ObjectList) GetObjects() []string{returno.objects
}
func (o *ObjectList) Size() int{return len(o.objects)
}
func (o *ObjectList) Get(index int) string{returno.objects[index]
}

测试调用

    /** 迭代器模式是给数据容器创建单独的迭代器,用来遍历里面的数据对象
* 数据容器和迭代器相互关联,外部通过迭代器来访问数据容器
* 通过这种方式由迭代器类来负责数据遍历,这样可以做到不暴露集合的内部结构
*/ int i = 0;
ObjectList objectList
= newObjectList();
objectList.setObjects(
new String[] { "Thomas", "Merry", "Jack", "Tony", "Jerry", "Joey"});//for循环迭代对象 for (Iterator iter =objectList.createIterator(); iter.hasNext();) {
String name
=(String) iter.next();
System.out.
println("objectList[" + i + "] =" +name);
i
++;
}
//while循环迭代对象 Iterator iter2 =objectList.createIterator();
objectList.setObjects(
new Integer[] { 3, 5, 7, 9, 11});
while (iter2.hasNext()) {
System.out.
println(iter2.next());
}

C语言简版

#include <stdio.h>#include<stdlib.h>

//简单版C语言迭代器模式,自己构建List数据类型//数据结构,这里使用链表作为示例
structList
{
char *data;struct List *next;
};
//迭代器结构体 structIterator
{
struct List *current;int (*has_next)(struct Iterator *); //判断是否还有下一个元素 char *(*next)(struct Iterator *, char **); //获取下一个元素 };//判断是否还有下一个元素 int has_next(struct Iterator *iter)
{
return iter->current !=NULL;
}
//获取下一个元素 char *next(struct Iterator *iter, char **value)
{
if (iter->current ==NULL)
{
returnNULL;
}
*value = iter->current->data;
iter
->current = iter->current->next;return *value;
}
//初始化迭代器 void create_iterator(struct Iterator *iter, struct List *head)
{
iter
->current =head;
iter
->has_next = &has_next;
iter
->next = &next;
}
//遍历链表 void iterate_list(struct List *head)
{
structIterator iter;char *value;
create_iterator(
&iter, head);while (iter.has_next(&iter))
{
iter.next(
&iter, &value);
printf(
"\r\n %s", value);
}
printf(
"\n");
}
intmain()
{
printf(
"test start:\r\n");//构造一个链表 struct List *head = (struct List *)malloc(sizeof(structList));
head
->data = "Tom";
head
->next = (struct List *)malloc(sizeof(structList));
head
->next->data = "Jerry";
head
->next->next = (struct List *)malloc(sizeof(structList));
head
->next->next->data = "Max";
head
->next->next->next =NULL;//使用迭代器遍历链表 iterate_list(head);//释放链表内存 while (head !=NULL)
{
struct List *temp =head;
head
= head->next;free(temp);
}
return 0;
}

更多语言版本

不同语言实现设计模式:
https://github.com/microwind/design-pattern

大家好,我是张飞洪,感谢您的阅读,我会不定期和你分享学习心得,希望我的文章能成为你成长路上的垫脚石,让我们一起精进。

在本文中,我们将讨论
ASP.NET Core
中的新路由。我们将了解什么是接口(
endpoints
)路由,它是如何工作的,它在哪里使用,以及如何创建自己的路由。

本文主题:

  • 探索接口路由
  • 创建自定义接口
  • 创建更复杂的接口

名词定义:端点,即我们访问的接口或者叫API,有些地方叫EndPoint或者叫接口,其实源头的称呼应该叫端点会更贴切一些。或者你也可以直接叫EndPoint,但是先不管那么多,大概了解这个意思就可以了。

探索接口路由

要了解接口路由(End Point),您需要了解什么是端点以及什么是路由。

端点是应用程序的一部分,当路由将传入的请求映射到它时,端点就会被执行。

客户端通常从服务器请求资源。大多数情况下,客户端是一个浏览器。资源由指向特定目标的URL定义。除了网页,它也可以是一个移动应用程序,从Web API请求特定JSON数据。

另一方面,执行的端点被映射到一个特定的路由,ASP.NET Core开发人员已经熟悉这样一种路由模式:

app.UseRouting(); 
app.UseAuthorization(); 
app.UseEndpoints(endpoints => {    
   endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); 
});

如果路由或路由模式与传入请求的URL匹配,则请求将映射到该端点。
ASP.NET Core
可以映射到以下端点:

  • Controllers (例如, MVC or web APIs)
  • Razor Pages
  • SignalR (and Blazor Server)
  • gRPC services
  • Health checks
    大多数端点都有非常简单的路由模式。只有MVC和Web API端点使用更复杂的模式。Razor页面的路由定义基于实际页面的文件夹和文件结构。

在ASP.NET Core 2.2中引入端点之前,路由只是运用在MVC和Web API中。Razor Pages中的隐式路由是内置的,SignalR没有路由一说。Blazor和gRPC在当时还不不知道在哪儿,健康检查最初是作为中间件组件实现的。

引入端点路由是为了将路由与实际端点分离,说得白话一点,就是让URL地址和实际执行的Action进行分离,这会让框架更加灵活,同时这意味着新的端点不需要实现自己的路由。

创建自定义接口

创建端点的最简单方法是使用lambda:

app.Map("/map", async context => {  
    await context.Response.WriteAsync("OK"); 
});

这里将/map路由映射到一个简单的端点,该端点将单词“OK”写入响应流。

关于早期
.NET 6.0
版本的说明

.NET 6.0
之前,该映射只能在Startup.cs文件中的UseEndpoints方法中,而使用
.NET 6.0
和新的
Minimal API
方法,可以在
Program.cs
文件中完成映射。

另外,我们需要将Microsoft.AspNetCore.Http命名空间添加到using语句中。
还可以将特定的HTTP方法(如GET、POST、PUT和DELETE)映射到端点。以下代码显示了如何映射GET和POST方法:

app.MapGet("/mapget", async context => {     
    await context.Response.WriteAsync("Map GET"); 
}); 
app.MapPost("/mappost", async context => {  
        await context.Response.WriteAsync("Map POST");
});

我们还可以将两个或多个HTTP方法映射到一个端点:

app.MapMethods("/mapmethods",  new[] { "DELETE", "PUT" }, 
    async context => {
        await context.Response.WriteAsync("Map Methods");
});

这些端点映射很像我们在第8篇
定制.NET 6.0的Middleware中间件
中看到的基于lambda的中间件组件,这些管道中间件会返回结果,例如基于HTML的视图、JSON结构化数据或类似的内容。但是,端点路由是一种更灵活的输出方式,它应该会在ASP.NET Core 3.0以后的所有版本中进行使用。

在第8篇中,我们看到我们可以像这样的分支管道:

app.Map("/map", mapped => {     // ……  });

以上这种方式也会创建一个路由,但只会侦听以
/map
开头的URL。如果您希望有一个处理
/map/{id:int?}
等模式的路由引擎,来匹配
/map/456
而不是
/map/abc
,那么您应该使用前面所述的新的路由。

而那些基于lambda的端点映射,对于简单的场景非常有用。然而,由于它们是在
Program.cs
中定义的,如果您想使用这种方式来实现更复杂的场景,代码维护性将变得很差。

因此,我们应该尝试找到一种更结构化的方法来创建自定义端点。

创建更复杂的接口

接下来,我们将创建一个健康检查接口例子,有点类似于您在Kubernetes集群中运行应用程序时可能需要的接口,用来检测系统的健康状态:
我们从开发者的角度定义API接口,我们首先添加一个MapMyHealthChecks方法,作为IEndpointRouteBuilder对象上的一个扩展方法,它没有实现:

app.MapMyHealthChecks("/myhealth");   
app.MapControllerRoute(name: "default",pattern:"{controller=Home}/{action=Index}/{id?}");

为了避免混淆,我们这儿采取和之前类似的方式添加新接口,后面我们进一步来实现一下。
我们创建一个名为
MapMyHealthChecksExtensions
的静态类,并在
MapMyHealthCheck
中放置一个扩展方法,该对象扩展
IEndpointRouteBuilder
接口并返回
IEndpointConventionBuilder
对象:

namespace RoutingSample; 
public static class MapMyHealthChecksExtensions {     
    public static IEndpointConventionBuilder  MapMyHealthChecks (this IEndpointRouteBuilder endpoints, string pattern = "/myhealth")     
    {         
        // ...     
    } 
}

以上只是骨架,实际的接口将被实现为一个终止的中间件,也就是说,它不调用下一个的中间件组件,并创建响应流的输出:

namespace RoutingSample; 
public class MyHealthChecksMiddleware {     
    private readonly ILogger _logger;     
    public MyHealthChecksMiddleware (RequestDelegate next, ILogger logger)    
     {  
        _logger = logger;     
    }     

    public async Task Invoke(HttpContext context)     {         
        // add some checks here...         
        context.Response.StatusCode = 200;         
        context.Response.ContentType = "text/plain";         
        await context.Response.WriteAsync("OK");     
    } 
}

实际工作是在
Invoke
方法中完成的。目前,只演示200状态码和OK状态响应,我们可以在这里随意扩展该方法,例如检查数据库或相关服务的可用性。

接下来我们使用这个终止中间件,我们回到
MapMyHealthChecks
方法的框架。我们现在创建一个自己的管道,并将其映射到给定的pipeline:

var pipeline = endpoints.CreateApplicationBuilder().UseMiddleware().Build(); 
return endpoints.Map(pattern, pipeline).WithDisplayName("My custom health checks");

这种方法允许我们为这个新的管道添加更多的中间件。
WithDisplayName
扩展方法将配置的显示名称设置为接口,接下来按F5键启动程序,并在浏览器中调用https://localhost:7666666/myhealth。我们将看到:

请注意,端口号可能会有所不同。我们还可以将已经存在的终止中间件组件转换为路由接口,以配置更加灵活的路由。

总结

ASP.NET Core
支持请求处理并向请求提供信息的多种方法。接口路由是一种基于URL和请求的方法提供资源。
在本文,我们学习了如何使用终止中间件组件作为接口,并用将该接口映射到新的路由引擎,从而让我们的路由变得更加强大和灵活。
每个Web应用程序都需要了解系统用户,以允许或限制对特定数据的访问。在下一章中,我们将展示如何配置身份验证和识别用户。