开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

SpringBoot 实战:在 RequestBody 中优

发表于 2021-11-29

「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」

本文被《Spring Boot 实战》专栏收录。

你好,我是看山。

在优雅的使用枚举参数(原理篇)中我们聊过,Spring对于不同的参数形式,会采用不同的处理类处理参数,这种形式,有些类似于策略模式。将针对不同参数形式的处理逻辑,拆分到不同处理类中,减少耦合和各种if-else逻辑。本文就来扒一扒,RequestBody参数中使用枚举参数的原理。

找入口

对 Spring 有一定基础的同学一定知道,请求入口是DispatcherServlet,所有的请求最终都会落到doDispatch方法中的ha.handle(processedRequest, response, mappedHandler.getHandler())逻辑。我们从这里出发,一层一层向里扒。

跟着代码深入,我们会找到org.springframework.web.method.support.InvocableHandlerMethod#invokeForRequest的逻辑:

1
2
3
4
5
6
7
8
9
java复制代码public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
        Object... providedArgs) throws Exception {

    Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
    if (logger.isTraceEnabled()) {
        logger.trace("Arguments: " + Arrays.toString(args));
    }
    return doInvoke(args);
}

可以看出,这里面通过getMethodArgumentValues方法处理参数,然后调用doInvoke方法获取返回值。getMethodArgumentValues方法内部又是通过HandlerMethodArgumentResolverComposite实例处理参数。这个类内部是一个HandlerMethodArgumentResolver实例列表,列表中是Spring处理参数逻辑的集合,跟随代码Debug,可以看到有27个元素。这些类也是可以定制扩展,实现自己的参数解析逻辑,这部分内容后续再做介绍。

选择Resolver

这个Resolver列表中,包含我们常用的几个处理类。Get请求的普通参数是通过RequestParamMethodArgumentResolver处理参数,包装类通过ModelAttributeMethodProcessor处理参数,RequestBody形式的参数,则是通过RequestResponseBodyMethodProcessor处理参数。这段就是Spring中策略模式的使用,通过实现org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter方法,判断输入参数是否可以解析。下面贴上RequestResponseBodyMethodProcessor的实现:

1
2
3
arduino复制代码public boolean supportsParameter(MethodParameter parameter) {
    return parameter.hasParameterAnnotation(RequestBody.class);
}

可以看到,RequestResponseBodyMethodProcessor是通过判断参数是否带有RequestBody注解来判断,当前参数是否可以解析。

解析参数

RequestResponseBodyMethodProcessor继承自AbstractMessageConverterMethodArgumentResolver,真正解析RequestBody参数的逻辑在org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters方法中。我们看下源码(因为源码比较长,文中仅留下核心逻辑。):

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
java复制代码protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
        Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
    MediaType contentType = inputMessage.getHeaders().getContentType();// 1
    Class<?> contextClass = parameter.getContainingClass();// 2
    Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);// 3

    Object body = NO_VALUE;

    EmptyBodyCheckingHttpInputMessage message = new EmptyBodyCheckingHttpInputMessage(inputMessage);// 4
    for (HttpMessageConverter<?> converter : this.messageConverters) {// 5
        Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
        GenericHttpMessageConverter<?> genericConverter =
                (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
        if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
                (targetClass != null && converter.canRead(targetClass, contentType))) {
            if (message.hasBody()) {
                HttpInputMessage msgToUse =
                        getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
                body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
                        ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));// 6
                body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
            }
            else {
                body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
            }
            break;
        }
    }
    return body;
}

跟着代码说明一下各部分用途:

  1. 获取请求content-type
  2. 获取参数容器类
  3. 获取目标参数类型
  4. 将请求参数转换为EmptyBodyCheckingHttpInputMessage类型
  5. 循环各种RequestBody参数解析器,这些解析器都是HttpMessageConverter接口的实现类。Spring对各种情况做了全量覆盖,总有一款适合的。文末给出HttpMessageConverter各个扩展类的类图。
  6. for循环体中就是选择一款适合的,进行解析
    1. 首先调用canRead方法判断是否可用
    2. 判断请求请求参数是否为空,为空则通过AOP的advice处理一下空请求体,然后返回
    3. 不为空,先通过AOP的advice做前置处理,然后调用read方法转换对象,在通过advice做后置处理

Spring的AOP不在本文范围内,所以一笔带过。后续有专题说明。

本例中,HttpMessageConverter使用的是MappingJackson2HttpMessageConverter,该类继承自AbstractJackson2HttpMessageConverter。看名称就知道,这个类是使用Jackson处理请求参数。其中read方法之后,会调用内部私有方法readJavaType,下面给出该方法的核心逻辑:

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
java复制代码private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
    MediaType contentType = inputMessage.getHeaders().getContentType();// 1
    Charset charset = getCharset(contentType);

    ObjectMapper objectMapper = selectObjectMapper(javaType.getRawClass(), contentType);// 2
    Assert.state(objectMapper != null, "No ObjectMapper for " + javaType);

    boolean isUnicode = ENCODINGS.containsKey(charset.name()) ||
            "UTF-16".equals(charset.name()) ||
            "UTF-32".equals(charset.name());// 3
    try {
        if (isUnicode) {
            return objectMapper.readValue(inputMessage.getBody(), javaType);// 4
        } else {
            Reader reader = new InputStreamReader(inputMessage.getBody(), charset);
            return objectMapper.readValue(reader, javaType);
        }
    }
    catch (InvalidDefinitionException ex) {
        throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
    }
    catch (JsonProcessingException ex) {
        throw new HttpMessageNotReadableException("JSON parse error: " + ex.getOriginalMessage(), ex, inputMessage);
    }
}

跟着代码说明一下各部分用途:

  1. 获取请求的content-type,这个是Spring实现的扩展逻辑,根据不同的content-type可以选择不同的ObjectMapper实例。也就是第2步的逻辑
  2. 根据content-type和目标类型,选择ObjectMapper实例。本例中直接返回的是默认的,也就是通过Jackson2ObjectMapperBuilder.cbor().build()方法创建的。
  3. 检查请求是否是unicode字符,目前来说,大家用的都是UTF-8的
  4. 通过ObjectMapper将请求json转换为对象。其实这部分还有一段判断inputMessage是否是MappingJacksonInputMessage实例的,考虑到大家使用的版本,这部分就不说了。

至此,Spring的逻辑全部结束,似乎还是没有找到我们使用的JsonCreator注解或者JsonDeserialize的逻辑。不过也能想到,这两个都是Jackson的类,那必然应该是Jackson的逻辑。接下来,就扒一扒Jackson的转换逻辑了。

深入Jackson的ObjectMapper逻辑

牵扯Jackson的逻辑主要分布在AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters和ObjectMapper#readValue这两个方法中。先说一下ObjectMapper#readValue方法的逻辑,这里面会调用GenderIdCodeEnum#create方法,完成类型转换。

ObjectMapper#readValue方法直接调用了当前类中的_readMapAndClose方法,这个方法里面比较关键的是ctxt.readRootValue(p, valueType, _findRootDeserializer(ctxt, valueType), null),这个方法就是将输入json转换为对象。咱们再继续深入,可以找到Jackson内部是通过BeanDeserializer这个类转换对象的,比较重要的是deserializeFromObject方法,源码如下(删除一下不太重要的代码):

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
java复制代码public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) throws IOException {
    // 这里根据上下文中目标类型,创建实例对象,其中 _valueInstantiator 是 StdValueInstantiator 实例。
    final Object bean = _valueInstantiator.createUsingDefault(ctxt);
    // [databind#631]: Assign current value, to be accessible by custom deserializers
    p.setCurrentValue(bean);

    if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
        String propName = p.currentName();
        do {
            p.nextToken();

            // 根据字段名找到 属性对象,对于gender字段,类型是 MethodProperty。
            SettableBeanProperty prop = _beanProperties.find(propName);
            if (prop != null) { // normal case
                try {
                    // 开始进行解码操作,并将解码结果写入到对象中
                    prop.deserializeAndSet(p, ctxt, bean);
                } catch (Exception e) {
                    wrapAndThrow(e, bean, propName, ctxt);
                }
                continue;
            }
            handleUnknownVanilla(p, ctxt, bean, propName);
        } while ((propName = p.nextFieldName()) != null);
    }
    return bean;
}

咱们看一下MethodProperty#deserializeAndSet的逻辑(只保留关键代码):

1
2
3
4
5
6
7
8
java复制代码public void deserializeAndSet(JsonParser p, DeserializationContext ctxt,
        Object instance) throws IOException {
    Object value;
    // 调用 FactoryBasedEnumDeserializer 实例的解码方法
    value = _valueDeserializer.deserialize(p, ctxt);
    // 通过反射将值写入对象中
    _setter.invoke(instance, value);
}

其中_valueDeserializer是FactoryBasedEnumDeserializer实例,快要接近目标了,看下这段逻辑:

1
2
3
4
5
6
java复制代码public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
    // 获取json中的值
    Object value = _deser.deserialize(p, ctxt);
    // 调用 GenderIdCodeEnum#create 方法
    return _factory.callOnWith(_valueClass, value);
}

_factory是AnnotatedMethod实例,主要是对JsonCreator注解定义的方法的包装,然后callOnWith中调用java.lang.reflect.Method#invoke反射方法,执行GenderIdCodeEnum#create。

至此,我们终于串起来所有逻辑。

文末总结

本文通过一个示例串起来@JsonCreator注解起作用的逻辑,JsonDeserializer接口的逻辑与之类型,可以耐心debug一番。下面给出主要类的类图:

图片

图片

图片

推荐阅读

  • SpringBoot 实战:一招实现结果的优雅响应
  • SpringBoot 实战:如何优雅的处理异常
  • SpringBoot 实战:通过 BeanPostProcessor 动态注入 ID 生成器
  • SpringBoot 实战:自定义 Filter 优雅获取请求参数和响应结果
  • SpringBoot 实战:优雅的使用枚举参数
  • SpringBoot 实战:优雅的使用枚举参数(原理篇)
  • SpringBoot 实战:在 RequestBody 中优雅的使用枚举参数
  • SpringBoot 实战:在 RequestBody 中优雅的使用枚举参数(原理篇)
  • SpringBoot 实战:JUnit5+MockMvc+Mockito 做好单元测试
  • SpringBoot 实战:加载和读取资源文件内容

你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

ElasticSearch从入门到精通(持续更新)—

发表于 2021-11-29

这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战。

from+size

我们知道ElasticSearch分页的时候如果用from+size的话,from + size 默认不能超过1万条数据。在深度分页的情况下,这种使用方式效率是非常低的,比如
from = 5000, size=10, es 需要在各个分片上匹配排序并得到5000*10条有效数据,然后在结果集中取最后10条,
es默认是10000条数据,可以通过设置max_result_window 值来改变最大数据量。

1
2
3
4
5
6
7
8
bash复制代码GET book_will/_search
{
"from": 0,
"size": 200
"query": { "match_all": {}},
"sort" : ["_doc"],
"size": 1000
}

scroll

scroll 查询 可以用来对 Elasticsearch 有效地执行大批量的文档查询,而又不用付出深度分页那种代价。游标查询允许我们 先做查询初始化,然后再批量地拉取结果。 这有点儿像传统数据库中的 cursor。

原理上是对某次查询生成一个游标 scroll_id , 后续的查询只需要根据这个游标去取数据,直到结果集中返回的 hits 字段为空,就表示遍历结束。scroll_id 的生成可以理解为建立了一个临时的历史快照,在此之后的增删改查等操作不会影响到这个快照的结果。scroll方式官方的建议并不是用于实时的请求,因为每一个 scroll_id 不仅会占用大量的资源(特别是排序的请求),而且是生成的历史快照,对于数据的变更不会反映到快照上。这种方式往往用于非实时处理大量数据的情况,比如要进行数据迁移或者索引变更之类的。

1
2
3
4
5
6
sql复制代码GET book_will/_search?scroll=1m 
{
"query": { "match_all": {}},
"sort" : ["_doc"],
"size": 1000
}

tips:

scroll=1m 保持游标查询窗口一分钟。

“sort” : [“_doc”] 关键字 _doc 是最有效的排序顺序。

这个查询的返回结果包括一个字段 _scroll_id, 它是一个base64编码的长字符串 。

5002.png

现在我们能传递字段 _scroll_id 到 _search/scroll 查询接口获取下一批结果:

1
2
3
4
5
sql复制代码GET /_search/scroll
{
"scroll": "1m",
"scroll_id" : "cXVlcnlUaGVuRmV0Y2g7NTsxMDk5NDpkUmpiR2FjOFNhNnlCM1ZDMWpWYnRROzEwOTk1OmRSamJHYWM4U2E2eUIzVkMxalZidFE7MTA5OTM6ZFJqYkdhYzhTYTZ5QjNWQzFqVmJ0UTsxMTE5MDpBVUtwN2lxc1FLZV8yRGVjWlI2QUVBOzEwOTk2OmRSamJHYWM4U2E2eUIzVkMxalZidFE7MDs="
}

注意再次设置游标查询过期时间为一分钟。

这个游标查询返回的下一批结果。 尽管我们指定字段 size 的值为1000,我们有可能取到超过这个值数量的文档。 当查询的时候, 字段 size 作用于单个分片,所以每个批次实际返回的文档数量最大为 size * number_of_primary_shards 。

search_after

在第一次查询时,记录上一次查询的位置,在接下来的查询中获取到上次查询的位置,接着查询;第二次查询在第一次查询时的语句基础上添加search_after,并指明从哪个数据后开始读取。将search_after指定的值作为查询条件(类似游标),指定从整个有序数据哪个位置继续查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码GET book_will/book/_search
{
"size": 10,
"query": {
"match" : {
"title" : "elasticsearch"
}
},
"search_after": [1463538857, "tweet#654323"],
"sort": [
{"es_timestamp": "asc"},
{"_uid": "desc"}
]
}

from+ size分页、scroll滚动搜索、search_after 假分页比较

1.from + size分页性能低;优点是灵活性好,实现简单;缺点是存在深分页问题;适用于数据量比较小,能容忍深度分页存在的场景

2.scroll滚动搜索性能中等;优点解决了深度分页问题;缺点是无法反应数据的实时性(快照版本),需要维护一个 scroll_id,成本高;适用于需要查询海量结果集场景

3.search_after 假分页性能高;优点性能最好,不存在深度分页问题,能够反映数据的实时变更;缺点是实现复杂,需要有一个全局唯一的字段,连续分页的时每一次查询都需要上次查询的结果;适用于需海量数据的分页的场景

深度分页.png

注意:

使用完记得清除scroll:
DELETE /_search/scroll/_all

1
2
3
4
ini复制代码//及时清除es快照,释放资源
ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
clearScrollRequest.addScrollId(scrollId);
this.esSafeRestClient.clearScroll(clearScrollRequest,RequestOptions.DEFAULT);

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

「用 macro! 实现逆波兰表达式」验证结果

发表于 2021-11-29

「这是我参与11月更文挑战的第 21 天,活动详情查看:2021最后一次更文挑战」


现在,任何词都被相应的分支处理,我们只需要处理最后的情况,即栈包含一个项目,并且没有更多的词了。

1
2
3
4
5
6
7
rust复制代码macro_rules! rpn {
// ...

([ $result:expr ]) => {
$result
};
}

此时,如果你用一个空的栈和RPN表达式来调用这个宏,它已经会产生一个正确的结果。

Playground

1
rust复制代码println!("{}", rpn!([] 2 3 + 4 *)); // 20

然而,我们的栈是一个实现细节,我们真的不希望每个消费者都传入一个空的堆栈,所以让我们在最后再添加一个接续分支,作为一个入口点,并自动添加[]。

Playground

1
2
3
4
5
6
7
8
9
rust复制代码macro_rules! rpn {
// ...

($($tokens:tt)*) => {
rpn!([] $($tokens)*)
};
}

println!("{}", rpn!(2 3 + 4 *)); // 20

我们的宏甚至适用于更复杂的表达式。

1
rust复制代码println!("{}", rpn!(15 7 1 1 + - / 3 * 2 1 1 + + -)); // 5

那如果出错了怎么办?现在,对于正确的RPN表达式来说,一切似乎都工作得很顺利,但对于一个可以投入生产的宏来说,我们需要确保它也能处理无效的输入,并且有合理的错误信息。

首先,让我们试着在中间插入另一个数字,看看会发生什么:

1
rust复制代码println!("{}", rpn!(2 3 7 + 4 *));

输出:

1
2
3
4
5
6
7
8
rust复制代码error[E0277]: the trait bound `[{integer}; 2]: std::fmt::Display` is not satisfied
--> src/main.rs:36:20
|
36 | println!("{}", rpn!(2 3 7 + 4 *));
| ^^^^^^^^^^^^^^^^^ `[{integer}; 2]` cannot be formatted with the default formatter; try using `:?` instead if you are using a format string
|
= help: the trait `std::fmt::Display` is not implemented for `[{integer}; 2]`
= note: required by `std::fmt::Display::fmt`

好吧,这个输出看起来没有帮助,因为它没有提供任何与表达式中的实际错误相关的信息。

为了弄清发生了什么,我们需要对我们的宏进行调试。为此,我们将使用 trace_macros 功能(和其他可选的编译器功能一样,你需要一个Rust nightly版本)。我们不想跟踪 println! 调用,所以我们将把我们的RPN计算分离到一个变量中。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

35数据结构-栈的概念与实现(下)

发表于 2021-11-29

一.静态栈的缺陷

当存储的元素为类类型的时候,静态栈会的对象在创建的时候会多次调用元素类型的构造函数,影响效率,当使用原生数组作为存储空间,在创建创建栈的时候会调用泛指类型T的构造函数,当函数退出的时候又调用析构函数

1.1 测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
c复制代码#include <iostream>
#include "staticStack.h"

using namespace std;
class Test
{
public:
Test()
{
cout << "Test()" << endl;
}

~Test()
{
cout << "~Test()" << endl;
}
};

int main()
{
staicStack<Test, 5> stack;

cout << stack.size() << endl;
}

结果为:
在这里插入图片描述
从结果来看,此时栈中没有任何元素,但是却调用了5次构造函数和析构函数

二.链式栈的实现

实际上就是单链表,定义一个top指针,始终指向链表的首元素,当入栈的时候将新结点的next指针指向top指针,并移动top指针,出栈的直接摧毁首结点即可

2.1 设计要点

  1. 抽象父类Stack的直接子类
  2. 在内部组合使用LinkList类,即链表类,实现栈的链式存储
  3. 只在单链表的头部进行操作
    在这里插入图片描述

2.2 入栈

单链表的头插法

1
2
3
4
5
c复制代码	void push(const T& e)
{
//直接使用头插法插入链表元素
m_list.insert(0, e);
}

2.3 出栈

移除下标为0的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
c复制代码	void pop()
{
//出栈直接弹出下标为0的结点
if (m_list.length() > 0)
{
m_list.remove(0);

}
else
{
cout << "no node to pop" << endl;
}

}

2.4 获取栈顶元素

1
2
3
4
5
6
7
8
9
10
11
12
c复制代码	T top()const
{
if (m_list.length() > 0)
{
m_list.get(0);

}
else
{
cout << "no node to pop" << endl;
}
}

2.5 清空栈

1
2
3
4
c复制代码	void clear()
{
m_list.clear();
}

2.6 栈成员个数

1
2
3
4
c复制代码	int size()
{
return m_list.length();
}

三.完整代码

3.1 LinkList.h

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
c复制代码#ifndef  __LINK_LIST_
#define __LINK_LIST_

#include <iostream>
using namespace std;

template<class T>
class LinkList
{
public:
struct Node
{
T value;
Node* next;
};

mutable struct
{
char reverse[sizeof(T)];
Node* next;
} m_header;

int m_length;
int m_step;
Node* m_current;

virtual Node* create()
{
return new Node();
}


void destroy(Node* pn)
{
delete pn;
}


public:

LinkList()
{
m_header.next = NULL;
m_length = 0;
m_step = 0;
m_current = NULL;
}

Node* position(int i)const
{
Node* pre = reinterpret_cast<Node*>(&m_header);
for (int pos = 0; pos < i; pos++)
{
pre = pre->next;
}

return pre;
}



int find(const T& e)const
{
Node* pre = reinterpret_cast<Node*>(&m_header);
int i = 0;

while (e != pre->next->value)
{
pre = pre->next;
i++;
}

return i;
}


bool end()
{
return m_current == NULL;
}



bool move(int i, int step = 1)
{
bool ret = (i >= 0) && (i <= m_length);
if (ret)
{
m_current = position(i)->next;
m_step = step;
}

return ret;
}



T current()
{

if (!end())
{
return m_current->value;
}
else
{
cout << "current end()" << endl;
return -1;//不知道写啥值
}
}


bool next()
{
int i = 0;
while (!end() && i < m_step)
{
m_current = m_current->next;
i++;
}

return i == m_step;
}



bool insert(int i, const T& e)
{
bool ret = (i >= 0) && (i <= m_length);
if (ret)
{
Node* node = create();

if (node != NULL)
{
Node* pre = reinterpret_cast<Node*>(&m_header);

pre = position(i);

node->value = e;
node->next = pre->next;
pre->next = node;
}
else
{
cout << "no memery to malloc" << endl;
}
}

m_length++;
cout << "m_l=" << m_length << endl;
return ret;
}



bool remove(int i)
{
bool ret = (i >= 0) && (i <= m_length);

if (ret)
{
Node* pre = reinterpret_cast<Node*>(&m_header);

pre = position(i);

Node* toDel = pre->next;
pre->next = toDel->next;
delete toDel;
m_length--;
}

return ret;
}


bool set(int i, T& e)
{
bool ret = (i >= 0) && (i <= m_length);

if (ret)
{
Node* pre = reinterpret_cast<Node*>(&m_header);

pre = position(i);

pre->next->value = e;
}

return ret;
}


T get(int i) const
{
bool ret = (i >= 0) && (i <= m_length);
T e;

if (ret)
{
Node* pre = reinterpret_cast<Node*>(&m_header);

pre = position(i);

e = pre->next->value;
}

return e;
}



int length() const
{
return m_length;
}


void clear()
{

while (m_header.next != NULL)
{
Node* toDel = m_header.next;
m_header.next = toDel->next;
destroy(toDel);
}

m_length = 0;
}


~LinkList()
{
clear();
}

};



#endif

3.2 Stack.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c复制代码#ifndef STACK_H
#define STACK_H


template<class T>
class Stack
{
public:
virtual void push(const T& e) = 0;//入栈
virtual void pop() = 0;//出栈
virtual T top() const = 0;//返回栈顶元素
virtual void clear() = 0;//清空栈
virtual int size() = 0;//返回栈的大小
};

#endif

3.3 linkStack.h

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
c复制代码#pragma once
#include "LinkList.h"
#include "Stack.h"

template<class T>
class linkStack :public Stack<T>
{
protected:
LinkList<T> m_list;
public:
void push(const T& e)
{
//直接使用头插法插入链表元素
m_list.insert(0, e);
}

void pop()
{
//出栈直接弹出下标为0的结点
if (m_list.length() > 0)
{
m_list.remove(0);

}
else
{
cout << "no node to pop" << endl;
}

}

T top()const
{
if (m_list.length() > 0)
{
return m_list.get(0);
}
else
{
cout << "no node to pop" << endl;
}
}


void clear()
{
m_list.clear();
}

int size()
{
return m_list.length();
}
};

3.4 测试程序

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
c复制代码#include <iostream>

#include "linkStack.h"

using namespace std;


class Test
{
public:
Test()
{
cout << "Test()" << endl;
}

~Test()
{
cout << "~Test()" << endl;
}
};

int main()
{
linkStack<Test> stack;

cout << stack.size() << endl;
}

3.5 结果

在这里插入图片描述
从结果看到没有再调用构造函数

四:栈实践-符号检测

编译器是如何实现符号检测的呢,下面的实现思路

4.1 实现思路

  • 从第一个字符开始扫描
  • 当遇到普通字符则不管,遇到左符号则压栈,遇到右符号则弹出栈顶符号,并与右符号进行匹配
  • 成功:所有的字符都扫描,且栈为空
  • 失败:匹配失败且所有字符都扫描成功当栈为空

4.2 代码实现

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
c复制代码#include <iostream>

#include "linkStack.h"

using namespace std;


bool is_left(char c)
{
return (c == '(') || (c == '{') || (c == '[') || (c == '<');
}

bool is_rigth(char c)
{
return (c == ')') || (c == '}') || (c == ']') || (c == '>');
}

bool is_quot(char c)
{
return (c == '\'') || (c == '\"');
}

bool is_match(char l, char r)
{
return ((l == '(') && (r == ')')) ||
((l == '{') && (r == '}')) ||
((l == '[') && (r == ']')) ||
((l == '<') && (r == '>')) ||
((l == '\'') && (r == '\'')) ||
((l == '\"') && (r == '\"'));
}


bool scan(const char* code)
{
linkStack<char> stack;
bool ret = true;
int i = 0;

code = (code == NULL) ? "" : code;

while (code[i] != '\0')
{
//左符号则入栈
if (is_left(code[i]))
{
stack.push(code[i]);
}
else if (is_rigth(code[i]))
{
//遇见右符号,则弹出栈顶符号与其进行匹配
if ((stack.size() > 0 && is_match(stack.top(), code[i])))
{
stack.pop();
}
else
{
ret = false;
}
}
else if (is_quot(code[i]))
{
//如果栈是空,或者当前的引号是左字符(也就是和栈顶不匹配),则入栈
if ((stack.size() == 0) || (!is_match(stack.top(),code[i])))
{

stack.push(code[i]);
}
else if(is_match(stack.top(), code[i]))
{
//匹配上了
stack.pop();
}
}
i++;
}

return ret && (stack.size() == 0);
}


int main()
{

cout << scan("<{a}{b(\'x\')c}d>") << endl;
return 0;
}

结果:
在这里插入图片描述

五.小结

  • 栈后进先出的特性适合检测成对出现的符号
  • 栈适合就近匹配的场合

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

「用 macro! 实现逆波兰表达式」运算符计算

发表于 2021-11-29

「这是我参与11月更文挑战的第 21 天,活动详情查看:2021最后一次更文挑战」


不过我们仍然缺少对操作员的支持。我们如何匹配运算符?

如果我们的RPN是一连串的标记,我们希望以完全相同的方式来处理,我们可以简单地使用$($token:tt)* 这样的列表。不幸的是,这不能让我们通过列表,根据每个标记推送一个操作数或应用一个运算符。

书中说 “宏系统完全不处理解析的模糊性”,这对单个宏分支来说是正确的 —— 我们不能匹配一个数字序列后面的运算符,如 $($num:tt)*+,因为 + 也是一个有效的标记,可以被tt组匹配,但这又是递归宏存在的原因。

如果你的宏定义中有不同的分支,Rust会逐一进行尝试,所以我们可以把我们的操作符分支放在数字分支之前,这样就可以避免任何冲突:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
rust复制代码macro_rules! rpn {
([ $($stack:expr),* ] + $($rest:tt)*) => {
// TODO
};

([ $($stack:expr),* ] - $($rest:tt)*) => {
// TODO
};

([ $($stack:expr),* ] * $($rest:tt)*) => {
// TODO
};

([ $($stack:expr),* ] / $($rest:tt)*) => {
// TODO
};

([ $($stack:expr),* ] $num:tt $($rest:tt)*) => {
rpn!([ $num $(, $stack)* ] $($rest)*)
};
}

正如我前面所说,运算符被应用于堆栈上的最后两个数字,所以我们需要分别匹配它们,”计算” 结果(构建一个正则的infix表达式)并将其放回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
rust复制代码macro_rules! rpn {
([ $b:expr, $a:expr $(, $stack:expr)* ] + $($rest:tt)*) => {
rpn!([ $a + $b $(, $stack)* ] $($rest)*)
};

([ $b:expr, $a:expr $(, $stack:expr)* ] - $($rest:tt)*) => {
rpn!([ $a - $b $(, $stack)* ] $($rest)*)
};

([ $b:expr, $a:expr $(, $stack:expr)* ] * $($rest:tt)*) => {
rpn!([ $a * $b $(,$stack)* ] $($rest)*)
};

([ $b:expr, $a:expr $(, $stack:expr)* ] / $($rest:tt)*) => {
rpn!([ $a / $b $(,$stack)* ] $($rest)*)
};

([ $($stack:expr),* ] $num:tt $($rest:tt)*) => {
rpn!([ $num $(, $stack)* ] $($rest)*)
};
}

不过我不是很喜欢这种明显的重复,但是就像字面意思一样,没有特殊的标记类型来匹配运算符。

然而,我们可以做的是,添加一个负责计算的辅助函数,并将任何显式运算符分支委托给它。

在宏中,你不能真正使用外部辅助函数,但你唯一能确定的是你的宏已经在范围内了,所以通常的技巧是在同一个宏中用一些独特的标记序列 “标记” 一个分支,然后像我们在常规分支中那样递归地调用它。

让我们使用 @op 作为这样的标记,并通过它里面的 tt 接受任何运算符(tt 在这种情况下是明确的,因为我们将只向这个助手传递运算符)。

而且,栈不再需要在每个单独的分支中展开 —— 因为我们在前面将其包裹在[]括号中,它可以作为另一个标记树(tt)进行匹配,然后传递到我们的辅助函数。

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
rust复制代码macro_rules! rpn {
(@op [ $b:expr, $a:expr $(, $stack:expr)* ] $op:tt $($rest:tt)*) => {
rpn!([ $a $op $b $(, $stack)* ] $($rest)*)
};

($stack:tt + $($rest:tt)*) => {
rpn!(@op $stack + $($rest)*)
};

($stack:tt - $($rest:tt)*) => {
rpn!(@op $stack - $($rest)*)
};

($stack:tt * $($rest:tt)*) => {
rpn!(@op $stack * $($rest)*)
};

($stack:tt / $($rest:tt)*) => {
rpn!(@op $stack / $($rest)*)
};

([ $($stack:expr),* ] $num:tt $($rest:tt)*) => {
rpn!([ $num $(, $stack)* ] $($rest)*)
};
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Boot RocketMq的食用方法 前言

发表于 2021-11-29

这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战

前言

之前我们讲了rocketmq的概念,让我们书接上文,这次来讲一下RocketMQ的具体使用方法,目前公司主推的MQ就是RocketMQ,前前后后看了一点点源码,用了一个月,讲一下基础部分。可以满足大家的日常业务需求。

image.png

回顾

RocketMQ是一个高性能支持分布式的搭建的一款消息队列,他的吞吐量非常高,是阿里巴巴开源的一款中间件,在我上大学时,当时同学做毕设有一个问题:我用的python写的后端服务器如何跟你的java进行通讯呢?当时由于技术力过低最后放弃了异构,但是RocketMQ就可以做到通讯的一个作用,完美的解决上述问题,当然MQ有许多的应用场景,上述只是其中一种,它还可以用来做削峰填谷等抗住并发压力,给服务器带来高性能回报。

前提

在开始使用MQ之前我们需要导入MQ的包,这里由于我是公司的项目,所以我采用的包是官方给出的starter自动装配包,配合SpringBoot可以快速搭建项目,实现敏捷开发。当然你也可以选择导入原生MQ包,详细请读者去Maven中央仓库自行获取。

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>

consumer

引入Mq jar包后,点击jar包可以发现他的文件中有一个这个注解

image.png

通过这个注解我们可以实现简单地消费者监听,配合topic实现连接MQ Broker来获取信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
less复制代码@Component
@RocketMQMessageListener( //重点注解
consumerGroup = "wopen_gateway_consumer_group",
topic = "${stong.wopen.rocketmq.topic.gateway}") // 采用yml来注入值
@Slf4j
public class GatewayConsumer implements RocketMQListener<DeviceVo>{

@Autowired
private GatewayService service;

@Autowired
private PushMessageService pushMessageService;

@Override
public void onMessage(DeviceVo gatewayVo) {

//业务逻辑处理
}

}

通过yml的形式来注入topic可以实现不修改代码情况下更换topic。

image.png

producer

生产者基于MQ的官方start包更便于开发者使用,这里演示同步方法的调用,异步与单向发送是一样的,只是返回结果不同而已。

1
2
3
4
5
kotlin复制代码@Autowired
private RocketMQTemplate mqTemplate; // 注入Mqtemplate

@Value("${wftp.rocketmq.topic.aerator}") // 注入需要发送的Topic 一样与上面消费者采用可配置topic
private String aeratorTopic;

就一句话 ,比之前的原生发送简单了一万倍,其他的异步与同步也是一样得,异步要注意一下有个回调需要你自己处理一下。

1
ini复制代码SendResult sendResult = mqTemplate.syncSend(aeratorTopic, 业务数据);

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

「用 macro! 实现逆波兰表达式」匹配&入栈

发表于 2021-11-29

「这是我参与11月更文挑战的第 21 天,活动详情查看:2021最后一次更文挑战」


宏目前不允许匹配字面意义,而且expr对我们不起作用,因为它可能会意外地匹配像 2+3... 这样的序列,而不是只取一个数字,所以我们将求助于 tt —— 一个通用的标记匹配器。

它只匹配一个标记树(无论是像 literal/identifier/lifetime/etc 这样的原始标记还是包含更多标记的 ()/[]/{})。

1
2
3
4
5
rust复制代码macro_rules! rpn {
($num:tt) => {
// TODO
};
}

现在,我们需要一个栈的变量。

宏不能使用真正的变量,因为我们希望这个栈只在编译时存在。所以,取而代之的是,有一个单独的标记序列,它可以被传递,因此被用作一种累积器。

在我们的例子中,让我们把它表示为逗号分隔的expr序列(因为我们不仅要用它来表示简单的数字,还要用它来表示中间的infix表达式),并把它包在括号里,与输入的其他部分分开。

1
2
3
4
5
rust复制代码macro_rules! rpn {
([ $($stack:expr),* ] $num:tt) => {
// TODO
};
}

现在,一个标记序列并不是一个真正的变量 —— 你不能在原地修改它,然后再做一些事情。

相反,你可以用必要的修改来创建这个标记序列的新副本,然后再次递归调用同一个宏。

如果你有函数式语言的背景,或者曾经使用过任何提供不可变数据的库,那么这两种方法:通过创建一个修改过的副本和用递归处理列表来改变数据。

1
2
3
4
5
rust复制代码macro_rules! rpn {
([ $($stack:expr),* ] $num:tt) => {
rpn!([ $num $(, $stack)* ])
};
}

现在,很明显,只有一个数字的情况是相当不可能的,对我们来说也不是很实用,所以我们需要把这个数字之后的任何其他东西匹配成一串零或更多的tt标记,这些标记可以被传递给我们的宏的下一个调用,以便进一步匹配和处理。

1
2
3
4
5
rust复制代码macro_rules! rpn {
([ $($stack:expr),* ] $num:tt $($rest:tt)*) => {
rpn!([ $num $(, $stack)* ] $($rest)*)
};
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

「用 macro! 实现逆波兰表达式」开始

发表于 2021-11-29

「这是我参与11月更文挑战的第 21 天,活动详情查看:2021最后一次更文挑战」


在 Rust 众多有趣的功能特性中,Rust 宏无疑是最有趣的那个(个人觉得)。可惜的是,即使读完了这本书和各种教程,然后我试图实现一个处理不同元素的复杂列表的宏时,我仍然很难理解它应该如何做。花了一些时间,直到我到了那个 “叮” 的时刻,开始错误地使用宏来处理一切:) (好吧,不是像我所看到的那样,在所有的事情上都使用宏,因为我不想使用函数和指定类型和生命期,但在任何地方都是有用的)

因此,这里是我对描述编写此类宏背后的原则的看法。它假定你已经阅读了 书中的宏部分,并且熟悉基本的宏定义和标记类型。

在本教程中,我将以逆波兰表达式为例。它很有趣,因为它足够简单,你可能已经从学校熟悉了它,然而为了在编译时静态地实现它,你已经需要使用递归宏的方法。

逆波兰表达式(也叫后缀记数法)使用栈进行所有操作,因此任何操作数都被推到栈中,任何[二进制]运算符都从栈中取出两个操作数,评估结果并将其放回。因此,像下面这样的表达式:

1
rust复制代码2 3 + 4 *

翻译为:

  1. 把2放到栈里
  2. 把3放进栈里
  3. 从堆=栈中取出最后两个值(3和2),应用运算符+,将结果(5)放回栈中
  4. 把4放到栈中
  5. 从堆栈中取出最后两个值(4和5),应用运算符*(4*5),将结果(20)放回堆栈中
  6. 表达式结束,堆栈中的单个值就是结果(20)

在数学和大多数现代编程语言中使用的更常见的infix符号中,表达式看起来像 (2+3)*4。

所以让我们写一个宏,在编译时评估RPN,将其转换为Rust所理解的infix符号:

1
2
3
4
5
rust复制代码macro_rules! rpn {
// TODO
}

println!("{}", rpn!(2 3 + 4 *)); // 20

让我们从将数字推入栈开始。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

ISP 一键下载原理分析

发表于 2021-11-29

1 STM32F103RCT6最小系统BootLoader

  • BOOT1=0,BOOT0可变
    image.png
    image.png

2 ISP 一键下载原理分析

  • ISP 的时候需要用到(bootloader)自举程序,自举程序存储在 STM32 器件的内部自举ROM存储器(系统存储器)中。其主要任务是通过一种可用的串行外设(USART、 CAN、USB、 I2C等)将应用程序下载到内部Flash中。每种串行接口都定义了相应的通信协议,其中包含兼容的命令集和序列。
    image.png
    image.png

2.1 ISP普通下载方案一

    1. 电脑通过 USB 转串口线连接 STM32 的 USART1,并打开电脑端的上位机;
    1. 设置跳线保持 BOOT0 为高电平, BOOT1 为低电平;
    1. 复位单片机使其进入 bootloader 模式,通过上位机下载程序;
    1. 下载完毕,设置跳线保持 BOOT0 为低电平, BOOT1 为低电平;
    1. 复位单片机即可启动用户代码,正常运行。
      image.png

2.2 ISPISP一键下载方案二

image.png

  • 图中的 Q1 和 Q2 外加几个电阻和一个二极管就构成了开发板的一键下载电路,此电路通过RST 和 DTR 信号来控制 BOOT0 和 RESET 信号,从而实现一键下载的功能。很多朋友问一键下载的原理,这里和大家讲一下,先说一个前提:DTR_N 和 RTS_N 的输出和 DTR/RTS 的设置是相反的。必须先记下这个前提。
  • 一键下载电路的具体实现过程:*
  • 首先,mcuisp 控制 DTR 输出低电平,则 DTR_N 输出高,然后 RTS 置高,则RTS_N 输出低,这样Q2 导通了,BOOT0 被拉高,即实现设置 BOOT0 为 1,同时 Q1 也会导通,STM32 的复位脚被拉低,实现复位。
  • 然后,延时 100ms 后,mcuisp 控制DTR 为高电平,则 DTR_N 输出低电平,RTS 维持高电平,则 RTS_N 继续为低电平,此时 STM32的复位引脚,由于 Q1 不再导通,变为高电平,STM32 结束复位,但是 BOOT0 还是维持为 1,从而进入 ISP 模式,接着 mcuisp 就可以开始连接 STM32,下载代码了,从而实现一键下载。

2.3 ISPISP一键下载方案三

image.png

  • USB 转串口估计大家都很熟悉,一般都是用到 RXD 和 TXD 这两个口,一键 ISP电路中我们需要用 USB 转串口的芯片的DTR口和RTS口来控制单片机的BOOT0和NRST,原理如下:
    1. 通过上位机控制 U6(CH340G)的RTS脚为低电平,Q1导通,BOOT0的电平上拉为高电平。
    1. 通过上位机控制 U6(CH340G)的 DTR 脚为高电平,由于 RTS 为低电平, Q2 导通,U8 的 2 脚为低电平,U18 为一个模拟开关,使能端由 4 脚控制,默认高电平, U18的 1 脚和 2 脚导通,所以 NRST 为低电平系统复位。
    1. 单片机进入 ISP 模式,此时可以将 DTR 脚设置为低电平,RTS 设置为高电平。 Q1和 Q2 为截至状态, BOOT0 和 NRST 还原默认电平。
    1. 上位机将程序下载到单片机,下载完毕之后,程序自动运行。
    1. 至此,很多人还会认为 U18、 Q1、 Q2 是多余的,用 U6 的 RTS 和 DTR 直接控制也可以。正常情况下,这样理解没有问题,但是我们忽略了一点,就是单片机上电瞬间如果 USB 转串口连接了电脑, DTR 和 RTS 的电平是变化的,如果不处理好,单片机会一直进入 ISP 模式,或者系统会复位多次,这种情况是不允许的。
    1. 于是,就有了我们全新的一键 ISP 电路。我们主要是分析上电瞬间的逻辑关系,单片机上电时我们通过示波器观察波形得知 DTR 和 RTS 的电平是变化的,但是也有一个规律就是:只要 RTS 为低电平的时候, DTR 的电平也是低,因此一般情况 Q2不会导通,但由于这两个 IO 口的电平存在“竞争冒险”,会出现 RTS 的下降沿的时候刚好遇到 DTR 的上升沿,这个时候 Q2 导通,导致系统复位,而 BOOT0 此时有可能也为高电平,就会进入 ISP 模式。这个是不受我们控制的,我们不想系统出现这样的情况。因此加入了模拟开关来切断这种干扰。
    1. 加入模拟开关 U18,通过控制 U18 的 4 脚的开关来达到隔离干扰电平的目的。下面我们分析一下延时开关电路,上电瞬间,电容 C65 通过电阻 R18 来充电,由于电阻 100k 很大,电容的充电电流很小,等电容充电达到 U18 的 4 脚的有效电平 2V时,大概耗时 1S,在这个 1S 时间内 U18 的模拟开关是断开的,因此 RTS 和 DTR的干扰电平不会影响到系统复位。系统正常运行。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Python的从0到1(第二十二天)-Python函数及使用

发表于 2021-11-29

这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战

函数是什么

在 Python 中,函数是组织好的,可以重复使用,用来实现某一功能的代码。

与数学中的函数不同,在Python中,函数不是看上去冰冷无聊的规则和公式,而是有实打实的、有自己作用的代码。而且,它已经是你的老朋友了。

之所以说函数是我们的老朋友,是我们在先前的课堂中就已经接触到一些Python自带(学名叫“内置”)的函数了。

比如说当我们需要实现“打印”这个功能,我们会用到print();当我们需要实现“获取数据长度”这个功能,我们会要到len()。这些都是设定好了,可以直接拿过来就用的功能,这就叫做“组织好的代码”。

函数(Function)能实现的功能从简单到复杂,各式各样,但其本质是相通的:“喂”给函数一些数据,它就能内部消化,给你“吐”出你想要的东西。

这就像自动贩卖机,只不过贩卖机是喂点钱,吐出来一些吃的喝的用的东西;而Python函数则是喂各种各样的数据,吐出来各种各样的功能。

眼尖的话,你会发现图片里的函数后面都跟了个括号。而括号里放的东西,也就是我们需要输入的数据,它在函数中被称作【参数】。【参数】指向的是函数要接收、处理怎样的数据。

比如print()函数的括号里可以放不同的参数,根据你放的参数不同,print()函数会帮你在屏幕上显示不同的内容。

1
2
3
4
5
python复制代码print('Hello World')

print('我爱学Python')

print(42)

括号里面的字符串和整数,都是print() 函数的参数。

说了这么多函数的特征,现在我们一起看看函数的定义:

还是用贩卖机来打比方,贩卖机是设定好可以直接使用(组织好的),可以重复上架售卖不同的物品(重复使用),功能是卖东西(单一功能)。

而函数呢?以print()函数为例,它也是设定好可以直接使用(组织好的),不论你想打印什么参数都可以(重复使用),而print函数能实现的单一功能就是“打印”。

那除了 input()、print()…这些内置函数,我们总会需要内置函数满足不了的功能,这时又该怎么办呢?

定义和调用函数

想要认识函数,第一步,我们需要去定义一个函数。

定义函数

定义函数,就是自定义一个属于自己的函数,它的语法格式如下:

1
2
3
python复制代码def 函数名(参数):

代码块

咱们一个个来看,首先看到def:

定义函数时,我们要用到def语句,def 在英文中是 define 的缩写,意为“定义”,为定义函数的固定格式。

然后看到函数名:

和变量名一样,这里的函数名也是自己命名的,比如你在控制一辆中国最先进的坦克车-99G式,如何让它前进后退呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码# 实现前进功能

def forward():

print('前进!')

# 实现后退功能

def backward():

print('后退!')

# 实现开火功能

def fire():

print('开火!')

负责前进功能的零部件(函数)叫:forward()。而除了 forward() 以外的其它零部件(函数)也有其需要实现的功能:

backward() 负责刹车,fire() 负责开火。

而这里每个零件(函数)都有它们唯一固定的名称(函数名),它们也都可以处理一定的数据(参数)等。

这里的参数是函数要接收并对其进行处理的数据。

定义函数时,你可以像上面的代码一样不设置参数,但也可以设置很多个参数,关于它等会我们会细讲。

最后是代码块:

代码块就是函数内的代码内容,用于实现函数功能。当你使用函数时,就会执行代码块内的内容。

先讲不设置参数时的情况,这里拆解一个咱们坦克车操作中的零部件函数:forward() 来看。

在这段代码中:

函数名为:forward

代码块内容为:print(‘前进!’)

阅读完直接运行下列代码即可:

1
2
3
4
5
6
7
8
9
10
11
python复制代码#函数名:最好是取体现函数功能的名字,一般用小写字母和单下划线、数字等组合

def forward():

#参数:根据函数功能,括号里可以有多个参数,也可以不带参数,命名规则与函数名相同

#规范:括号是英文括号,后面的冒号不能丢

print('前进!')

#函数体:函数体就是体现函数功能的语句,要缩进,一般是四个空格

运行代码后为何没反应呢?按理来说应该已经成功定义这个函数了呀。

这是因为你还没有调用函数,调用函数就是我们常说的使用函数的功能,函数需要调用才能发挥作用。

调用函数

就像有了仪器,有了汽车,也需要先启动使用才能发挥它的功能。

回到 Python 中来,没设置参数时,调用函数的语法是:函数名()。调用函数以后,函数内的代码块就会自动执行。再试着运行下面这段代码,注意看最后一行代码:

1
2
3
4
5
6
7
python复制代码def forward():

print('前进!')

# 调用函数

forward()

看到代码块的内容被运行成功了吗?如果没问题的话,你就成功调用了这个你定义的函数。

函数的执行流程

那么你可能会有疑问,刚刚的那个函数是怎么执行的?一个函数的执行顺序又应该是怎样的呢?一起来看:

在Python执行定义函数的语句时,首先会创建函数,而不是执行该函数内部的内容。

每当调用对应的函数的时候,该函数内的代码块才会运行一遍,咱们来看看代码,运行一下:

1
2
3
4
5
6
7
8
9
python复制代码def forward():

print('前进!')

print('我会先被打印')

# 调用函数

forward()

看到终端显示的结果,调用函数以后才会执行函数内部的代码块。

调用函数的代码在第 5 行,所以第 3 行代码先被打印了出来,然后才轮到函数内代码块的内容被打印出来。

函数的创建和调用过程,如下

第一步:首先会创建函数。 在创建完函数以后,不会执行函数内代码块的内容。

第二步:会先执行其它代码,直到这个函数被调用。

然后第三步,到调用函数时,这个函数内的代码块才会运行。

总结一下,这段代码的运行过程分步来看是这样的:

1)先创建 print() 函数。

2)执行代码直到调用函数;

3)调用函数以后执行函数内部的代码块。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…116117118…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%