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

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


  • 首页

  • 归档

  • 搜索

Swift 6:导入语句上的访问级别

发表于 2024-04-26

前言

SE-0409 提案引入了一项新功能,即允许使用 Swift 的任何可用访问级别标记导入声明,以限制导入的符号可以在哪些类型或接口中使用。由于这些变化,现在可以将依赖项标记为对当前源文件(private 或 fileprivate)、模块(internal)、包(package)或所有客户端(public)可见。

此提案引入了两个功能标志后面的更改,这两个功能标志将在 Swift 6 中默认启用:

  • AccessLevelOnImport:这是一个已经可用的实验性功能标志,允许开发人员将导入声明标记为访问级别。
  • InternalImportsByDefault:这是一个即将推出的功能标志,目前尚不可用,它将导入语句的隐式访问级别从 public 更改为 internal,就像 Swift 6 将要做的那样。

这是语言中的一项很好的补充,我个人很长时间以来一直期待着,因为它可以帮助开发人员更好地隐藏实现细节并强制执行关注点分离。不仅如此,它还限制了包的客户端导入的依赖项数量,只允许满足一定条件的标记为 public 的依赖项导入,从而缩短了编译时间。

示例

假设我们创建了一个名为 Services 的 Swift 包,该包定义了一个 FeedService 目标。该目标的工作是获取要在应用程序中显示的项目的动态源。反过来,FeedService 依赖于另一个名为 FeedDTO 的目标,该目标定义了与 API 数据结构匹配的一组自动生成的可解码模型,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
swift复制代码// swift-tools-version: 5.10

import PackageDescription

let package = Package(
name: "Services",
platforms: [.iOS(.v13), .macOS(.v10_15)],
products: [
.library(
name: "FeedService",
targets: ["FeedService"]
),
],
targets: [
.target(
name: "FeedService",
dependencies: ["FeedDTO"]
),
.target(
name: "FeedDTO"
)
]
)

FeedDTO 目标的代码非常简单,并且是基于 OpenAPI 规范自动生成的:

1
2
3
4
5
6
7
8
9
10
11
swift复制代码import Foundation

public struct Feed: Decodable {
let items: [Item]

public struct Item: Decodable {
let title: String
let image: URL
let body: String
}
}

FeedService 目标并不复杂,它包含一个协议,该协议定义了服务的接口,供客户端使用。该协议的实现也属于 FeedService 目标,但对于本例来说并不重要,FeedService.swift 文件代码如下:

1
2
3
4
5
swift复制代码import FeedDTO

public protocol FeedService {
func fetch() -> Feed
}

正如你所看到的,我们在服务的公共接口中包含了 FeedDTO 目标中的 Feed 模型。由于在 Swift 5 中,所有导入声明都隐式为 public,并且没有办法更改此行为,上述代码可以编译而不会出现任何问题。尽管如此,架构远非理想,我们被允许暴露实现细节,并且我们没有办法让编译器阻止此泄漏。

如果我们注意到这个问题并想要解决它,我们可以从公共接口中删除 Feed 模型,并创建一个领域模型,该模型将成为公共接口的一部分。服务的实际实现将负责将 FeedDTO.Feed 模型转换为领域模型。FeedService.swift 文件代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
swift复制代码import Foundation
import FeedDTO

public struct Feed {
let items: [Item]

public struct Item {
let title: String
let image: URL
let body: String
}
}

public protocol FeedService {
func fetch() -> Feed
}

尽管上述代码是朝着正确方向迈出的一步,但代码中没有明确说明 FeedDTO 模块在此文件中的用法是实现细节,不应该是模块的公共接口的一部分。这就是 Swift 6 的功能派上用场的地方。

启用 AccessLevelOnImport

启用 AccessLevelOnImport 实验性标志

让我们看看如何通过为导入语句添加访问级别来使前一节的代码更加明确,并防范未来的更改可能会在此文件中暴露实现细节。

在我们这样做之前,由于此功能仍在实验性标志后面,我们需要在我们的Swift包中启用它,Package.swift 文件代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
swift复制代码// swift-tools-version: 5.10

import PackageDescription

let package = Package(
name: "FeedService",
platforms: [.iOS(.v13), .macOS(.v10_15)],
products: [
.library(
name: "FeedService",
targets: ["FeedService"]
),
],
targets: [
.target(
name: "FeedService",
dependencies: ["FeedDTO"],
swiftSettings: [
.enableExperimentalFeature("AccessLevelOnImport")
]
),
.target(name: "FeedDTO")
]
)

如果你使用的是 Xcode 项目,则可以通过将 -enable-experimental-feature AccessLevelOnImport 标志添加到目标的 OTHER_SWIFT_FLAGS 构建设置中来启用该功能。

现在我们已经启用了该功能,我们可以在 FeedService.swift 文件中的导入语句中添加访问级别,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
swift复制代码import Foundation
private import FeedDTO

public struct Feed {
let items: [Item]

public struct Item {
let title: String
let image: URL
let body: String
}
}

public protocol FeedService {
func fetch() -> Feed
}

通过这个改变,如果我们再次在模块的公共接口中使用 FeedDTO,编译器将会报错。这是一种强制实现关注点分离和隐藏模块客户端的实现细节的绝佳方式。

请注意,你可以在同一个依赖项在目标中使用不同的访问级别。在执行优化和决定是否将依赖项带给模块的消费者时,构建系统将考虑最不限制的访问级别。

破坏性变更

与 SE-0409 引入的更改相关的一个重大破坏性变更是:导入语句的默认访问级别将从 public 更改为 internal。这意味着,如果你在模块的公共接口中包含来自依赖项的符号,你需要明确将导入语句标记为 public,以避免编译错误。

有一个第二个功能标志,你很快就可以在 Swift 工具链的主要分支上启用,称为 InternalImportsByDefault,以测试新的行为。当它正式发布时,你将能够在你的 Swift 包中启用它:

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
swift复制代码// swift-tools-version: 5.10

import PackageDescription

let package = Package(
name: "FeedService",
platforms: [.iOS(.v13), .macOS(.v10_15)],
products: [
.library(
name: "FeedService",
targets: ["FeedService"]
),
],
targets: [
.target(
name: "FeedService",
dependencies: ["FeedDTO"],
swiftSettings: [
.enableExperimentalFeature("AccessLevelOnImport"),
.enableUpcomingFeature("InternalImportsByDefault")
]
),
.target(name: "FeedDTO")
]
)

如果你使用的是 Xcode 项目,则可以通过将 -enable-upcoming-feature InternalImportsByDefault 标志添加到目标的 OTHER_SWIFT_FLAGS 构建设置中来启用该功能。

采用这些更改

在采用这些新更改时的最佳实践是首先在你的 Swift 包中启用 AccessLevelOnImport 功能标志,并开始将最严格的访问级别添加到所有的导入语句中,让编译器告诉你可能需要进行更改的地方。

这是一个为你执行此操作的小脚本,replace-imports.swift 文件代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
swift复制代码#!/usr/bin/swift

private import Foundation

let fileManager = FileManager.default
let currentDirectory = fileManager.currentDirectoryPath
let swiftFiles = fileManager.enumerator(atPath: currentDirectory)?
.compactMap { $0 as? String }
.filter { $0.hasSuffix(".swift") }

for file in swiftFiles ?? [] {
let filePath = "\(currentDirectory)/\(file)"
guard let content = try? String(contentsOfFile: filePath) else {
continue
}

let updatedContent = content
.replacingOccurrences(of: #"import (\w+)"#, with: "private import $1", options: .regularExpression)

try? updatedContent.write(toFile: filePath, atomically: true, encoding: .utf8)
}

如果你对你的公共接口和它们所暴露的内容感到满意,或者如果你发现当你打开 InternalImportsByDefault 即将推出的功能标志时,有很多编译错误你不想立即修复,你可以修改上述脚本以将 public 访问级别添加到所有导入语句中。

总结

该文章介绍了 Swift 6 中关于导入声明访问级别的新功能。SE-0409 提案引入了此功能,允许开发人员使用任何可用的访问级别标记导入声明,从而限制了导入的符号在哪些类型或接口中可以使用。这项功能通过两个功能标志实现,即 AccessLevelOnImport 和 InternalImportsByDefault,它们将在 Swift 6 中默认启用。文章通过示例说明了如何在 Swift 包中使用这些功能,并介绍了相关的破坏性变更。最后,文章提出了采用这些更改的最佳实践,并提供了一个小脚本来帮助开发人员执行相应的更改。

本文转载自: 掘金

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

从‘海量请求’到‘丝滑响应’:巧解海量httpclient请

发表于 2024-04-26
大家好,我是石头~


今天接到一个需求,其关键功能需要依托一个外部HTTP接口来实现,而且这项功能还是整个业务中请求频率最高的。


开完需求会后,我就开始头疼了。众所周知,在高并发场景下进行HTTP请求,会降低整个服务的性能,怎样进行性能优化,就成为了实现本次需求的核心了。

image.png

1、大量HTTP请求的弊端

在进行性能优化之前,我们先来了解下为什么大量HTTP请求,会造成服务性能下降。


这个我们可以从以下几方面来看:
  1. 网络资源竞争:服务端在向其他服务发起HTTP请求时,会产生网络带宽的竞争。特别是当请求量很大时,大量的数据包在网络中穿梭,容易导致网络带宽饱和,增加延迟,甚至产生网络拥塞,使得请求响应时间延长。
  2. 系统资源消耗:服务端在处理每个HTTP请求时,都需要占用CPU、内存、文件句柄等系统资源。特别是在并发请求较高时,服务端必须创建和维护多个连接,处理请求和解析响应,这些都会消耗大量系统资源。一旦资源耗尽,新进的请求将无法得到及时处理,严重影响服务性能。
  3. 高并发下的连接管理:对于每次HTTP请求,服务端通常需要创建一个新的TCP连接。如果连接创建和销毁过于频繁,会大大增加系统开销。如果不采取连接池等优化手段,服务端可能会因连接管理负担过重而降低性能。
  4. 请求排队与响应时间:服务端发起的HTTP请求也需要排队等待对方服务器的响应,尤其是在目标服务本身负载较大或者网络条件不佳的情况下,响应时间的增长将进一步拖慢服务端的处理速度,最终可能形成连锁反应,导致整个系统性能下降。
综上所述,大量HTTP请求就像高峰期的交通堵塞,不仅挤占了网络通道,也给服务器处理能力带来巨大挑战。


那么,针对这些问题,我们要怎样进行优化?

b812c8fcc3cec3fd17b0f03685f7ff3286942777.webp

2、大量HTTP请求的优化策略

由于团队内部采用的是httpclient,那接下来,我们就以httpclient为例进行优化。

策略一:连接池管理

如同高效有序的物流仓库,高效的httpclient请求离不开合理的连接池管理。通过设置合适的最大连接数、超时时间以及重试策略,我们可以避免频繁创建和关闭连接带来的性能损耗,同时也能应对突发的大流量请求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码    PoolingHttpClientConnectionManager connMgr = new PoolingHttpClientConnectionManager();
connMgr.setMaxTotal(200); // 设置最大连接数
connMgr.setDefaultMaxPerRoute(20); // 设置每个路由基础的默认最大连接数

RequestConfig requestConfig = RequestConfig.custom()
.setSocketTimeout(5000) // 设置SO_TIMEOUT,即从连接成功建立到读取到数据之间的等待时间
.setConnectTimeout(3000) // 设置连接超时时间
.setConnectionRequestTimeout(1000) // 设置从连接池获取连接的等待时间
.build();

CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connMgr)
.setDefaultRequestConfig(requestConfig)
.build();

策略二:异步化处理与线程池

面对大量的网络请求,同步处理方式可能会导致线程阻塞,影响整体性能。采用异步处理机制结合线程池技术,能够将请求放入队列并分配给空闲线程执行,从而大大提高系统的并发处理能力,降低响应时间。
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
java复制代码    // 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, // 核心线程数
20, // 最大线程数
60, // 空闲线程存活时间(单位秒)
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100)); // 工作队列

CloseableHttpClient httpClient = ...; // 初始化 HttpClient

List<String> urls = ...; // 要请求的URL列表

for (String url : urls) {
final String finalUrl = url;
Runnable task = () -> {
try (CloseableHttpResponse response = httpClient.execute(new HttpGet(finalUrl))) {
// 处理响应逻辑
} catch (IOException e) {
// 处理异常
}
};

executor.execute(task);
}

// 在所有任务完成后关闭线程池
executor.shutdown();
// 可以选择等待所有任务完成
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);

// 或者在特定条件下停止并强制关闭线程池
if (!executor.isTerminated()) {
executor.shutdownNow();
}

// 别忘了在最后关闭HttpClient资源
httpClient.close();

策略三:请求合并与批量处理

对于类似的或依赖关系不强的请求,可以考虑合并为一个请求或者批量处理,减少网络交互次数,显著提升效率。例如,利用HTTP/2的多路复用特性,或者对数据进行归类整合后一次性获取。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码    List<String> userIds = ...; // 用户ID列表

HttpUriRequest[] requests = userIds.stream()
.map(userId -> new HttpGet("http://example.com/api/user/" + userId))
.toArray(HttpUriRequest[]::new);

HttpRequestRetryHandler retryHandler = ...; // 自定义重试处理器
HttpClient httpClient = HttpClients.custom().setRetryHandler(retryHandler).build();

List<Future<HttpResponse>> futures = new ArrayList<>();
for (HttpUriRequest request : requests) {
futures.add(httpClient.execute(request, new FutureCallback<HttpResponse>() {...}));
}

// 等待所有请求完成并处理结果
for (Future<HttpResponse> future : futures) {
HttpResponse response = future.get();
// 处理每个用户的响应信息
}

策略四:缓存优化

对于部分不变或短期内变化不大的数据,可以通过本地缓存或分布式缓存(如Redis)来避免重复请求,既节省了带宽,也减轻了服务器压力。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码    LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(1000) // 设置缓存的最大容量
.expireAfterWrite(1, TimeUnit.HOURS) // 数据写入一小时后过期
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
// 如果缓存中没有该key,则通过httpclient请求获取数据并返回
CloseableHttpResponse response = httpClient.execute(new HttpGet("http://example.com/api/data/" + key));
return EntityUtils.toString(response.getEntity());
}
});

// 获取数据,如果缓存中有则直接返回,否则发起网络请求并将结果存入缓存
String data = cache.get("someKey");

3、结语

优化之路永无止境,每一个环节都可能存在更深层次的改进空间,希望上面的内容在当你遇到类似问题时,对你有所帮助~

**MORE | 更多精彩文章**

  • 面试官:说说单点登录都是怎么实现的?
  • 面试不慌张:一文读懂FactoryBean的实现原理
  • JWT vs Session:到底哪个才是你的菜?
  • JWT重放漏洞如何攻防?你的系统安全吗?
  • JWT:你真的了解它吗?

本文转载自: 掘金

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

如何在网页实现 TypeScript 编辑器?

发表于 2024-04-26

有的需求需要在网页上写代码。

比如在线执行代码的 playground:

或者在线面试:

如果让你实现网页版 TypeScript 编辑器,你会如何做呢?

有的同学说,直接用微软的 monaco editor 呀:

确实,直接用它就可以,但是有挺多地方需要处理的。

我们来试试看。

1
lua复制代码npx create-vite

创建个 vite + react 的项目。

安装依赖:

1
2
3
bash复制代码npm install

npm install @monaco-editor/react

这里用 @monaco-editor/react 这个包,它把 monaco editor 封装成了 react 组件。

去掉 main.tsx 里的 index.css

然后在 App.tsx 用一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
javascript复制代码import MonacoEditor from '@monaco-editor/react'

export default function App() {

const code = `import lodash from 'lodash';
function App() {
return <div>guang</div>
}
`;

return <MonacoEditor
height={'100vh'}
path={"guang.tsx"}
language={"typescript"}
value={code}
/>
}

跑下开发服务:

1
arduino复制代码npm run dev

试下看:

现在就可以在网页写 ts 代码了。

但是有报错:

jsx 语法不知道怎么处理。

这里明显要改 typescript 的 tsconfig.json。

怎么改呢?

这样:

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
javascript复制代码import MonacoEditor, { OnMount } from '@monaco-editor/react'

export default function App() {

const code = `import lodash from 'lodash';
function App() {
return <div>guang</div>
}
`;

const handleEditorMount: OnMount = (editor, monaco) => {
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
jsx: monaco.languages.typescript.JsxEmit.Preserve,
esModuleInterop: true,
})

}

return <MonacoEditor
height={'100vh'}
path={"guang.tsx"}
language={"typescript"}
onMount={handleEditorMount}
value={code}
/>
}

onMount 的时候,设置 ts 的 compilerOptions。

这里设置 jsx 为 preserve,也就是输入

输出
,保留原样。

如果设置为 react 会输出 React.createElement(“div”)。

再就是 esModuleInterop,这个也是 ts 常用配置。

默认 fs 要这么引入,因为它是 commonjs 的包,没有 default 属性:

1
javascript复制代码import * as fs from 'fs';

设置 esModuleInterop 会在编译的时候自动加上 default 属性。

就可以这样引入了:

1
javascript复制代码import fs from 'fs';

可以看到,现在 jsx 就不报错了:

还有一个错误:

没有 lodash 的类型定义。

写 ts 代码没提示怎么行呢?

我们也要支持下。

这里用到 @typescript/ata 这个包:

ata 是 automatic type acquisition 自动类型获取。

它可以传入源码,自动分析出需要的 ts 类型包,然后自动下载。

我们新建个 ./ata.ts,复制文档里的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
javascript复制代码import { setupTypeAcquisition } from '@typescript/ata'
import typescriprt from 'typescript';

export function createATA(onDownloadFile: (code: string, path: string) => void) {
const ata = setupTypeAcquisition({
projectName: 'my-ata',
typescript: typescriprt,
logger: console,
delegate: {
receivedFile: (code, path) => {
console.log('自动下载的包', path);
onDownloadFile(code, path);
}
},
})

return ata;
}

安装用到的包:

1
css复制代码npm install --save @typescript/ata -f

这里就是用 ts 包去分析代码,然后自动下载用到的类型包,有个 receivedFile 的回调函数里可以拿到下载的代码和路径。

然后在 mount 的时候调用下:

1
2
3
4
5
6
7
8
9
javascript复制代码const ata = createATA((code, path) => {
monaco.languages.typescript.typescriptDefaults.addExtraLib(code, `file://${path}`)
})

editor.onDidChangeModelContent(() => {
ata(editor.getValue());
});

ata(editor.getValue());

就是最开始获取一次类型,然后内容改变之后获取一次类型,获取类型之后用 addExtraLib 添加到 ts 里。

看下效果:

有类型了!

写代码的时候用到的包也会动态去下载它的类型:

比如我们用到了 ahooks,就会实时下载它的类型包然后应用。

这样,ts 的开发体验就有了。

再就是现在字体有点小,明明内容不多右边却有一个滚动条:

这些改下 options 的配置就好了:

scrollBeyondLastLine 是到了最后一行之后依然可以滚动一屏,关闭后就不会了。

minimap 就是缩略图,关掉就没了。

scrollbar 是设置横向纵向滚动条宽度的。

theme 是修改主题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
javascript复制代码return <MonacoEditor
height={'100vh'}
path={"guang.tsx"}
language={"typescript"}
onMount={handleEditorMount}
theme: "vs-dark",
value={code}
options={
{
fontSize: 16,
scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
scrollbar: {
verticalScrollbarSize: 6,
horizontalScrollbarSize: 6,
}
}
}
/>

好多了。

我们还可以添加快捷键的交互:


默认 cmd(windows 下是 ctrl) + j 没有处理。

我们可以 cmd + j 的时候格式化代码。

1
2
3
javascript复制代码editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyJ, () => {
editor.getAction('editor.action.formatDocument')?.run()
});

试下效果:

有同学可能问,monaco editor 还有哪些 action 呢?

打印下就知道了:

1
2
3
4
5
javascript复制代码editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyJ, () => {
// editor.getAction('editor.action.formatDocument')?.run()
let actions = editor.getSupportedActions().map((a) => a.id);
console.log(actions);
});

有 131 个:

用到再搜就行。

这样,我们的网页版 TypeScript 编辑器就完成了。

总结

有的需求需要实现网页版编辑器,我们一般都用 monaco editor 来做。

今天我们基于 @monaco-editor/react 实现了 TypeScript 编辑器。

可以在 options 里配置滚动条、字体大小、主题等。

然后 onMount 里可以设置 compilerOptions,用 addCommand 添加快捷键等。

并且我们基于 @typescript/ata 实现了自动下载用到的 ts 类型的功能,它会扫描代码里的 import,然后自动下载类型,之后 addExtraLib 添加到 ts 里。

这样在网页里就有和 vscode 一样的 ts 编写体验了。

为啥要研究这个呢?

因为我最近在开发 react playground,在左侧写代码,然后实时编译在右侧预览:

这是我小册 《React 通关秘籍》的一个项目,感兴趣的话可以上车一起做。

本文转载自: 掘金

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

Android TextView的颜色和字体自适应

发表于 2024-04-26

前言

最近比较忙,没有时间去梳理一些硬核的东西,今天就来分享一些简单有意思的技巧。拿TextView来说,平时我们都会在特定的场景去设置它的字体颜色和字体大小。那么有没有一种办法能够一劳永逸不用每次都去设置,能让TextView自己去根据自身的控件属性去自适应颜色和大小呢?当然是有的,这里可以简单的分享一些思路。

1. 字体大小自适应

TextView可以根据让字体的大小随着宽高进行自适应。

设置大小自适应的方式很简单,只需要添加这3行代码即可

1
2
3
ini复制代码android:autoSizeMaxTextSize="22dp"  
android:autoSizeMinTextSize="8dp"
android:autoSizeTextType="uniform"

我们可以来看看效果,我给宽高都设置不同的值,能看到字体大小变化的效果

1
2
ini复制代码android:layout_width="50dp"  
android:layout_height="20dp"

image.png

1
2
ini复制代码android:layout_width="50dp"  
android:layout_height="30dp"

image.png

1
2
ini复制代码android:layout_width="50dp"  
android:layout_height="50dp"

image.png

1
2
ini复制代码android:layout_width="80dp"  
android:layout_height="80dp"

image.png

最后这里可以看到autoSizeMaxTextSize的效果

这里可以多提一句,一般这种字体随宽高自适应的场景在正常开发中比较少见。如果你的项目合理的话,一般字体的大小都是固定那几套,所以把字体大小定义到资源文件中,甚至通过style的方式去设置,才是最节省时间的方式。

2. 字体颜色自适应

关于字体的颜色自适应,如果你真想把这套东西搞起来,你就需要对“颜色”这个概念有一定的深层次的了解。我这里就只简单做一些效果来举例。

我这里演示Textview根据背景颜色来自动设置字体颜色是白色还是黑色,当背景颜色是暗色时(比如黑色),字体颜色变成白色,当背景颜色是亮色时(比如白色),字体颜色变成黑色。

那么首先需要有个概念:我怎么判断背景是亮色还是暗色?

这就需要对颜色有一定的理解。要判断一个颜色是暗色还是亮色,可以通过计算颜色的亮度来实现。一种常见的方法是将RGB颜色值转换为灰度值,然后根据灰度值来判断颜色的深浅程度。

灰度值的计算公式 灰度值 = 0.2126 * R + 0.7152 * G + 0.0722 * B

根据这个公式,我们能封装一个判断颜色是否是亮色的方法

1
2
3
4
5
6
7
kotlin复制代码private fun isLightColor(color: Int): Boolean {  
val r = color shr 16 and 0xFF
val g = color shr 8 and 0xFF
val b = color and 0xFF
val luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255
return luminance > 0.5
}

如果觉得这个判断不太符合你心里的预期,可以修改最后一行的luminance > 0.5值

下一步,我们需要获取控件的背景,然后从背景中获取颜色值。

获取背景直接调用

1
ini复制代码val d = textView?.background

根据Drawable去获取颜色

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码private fun getColorByDrawable(d : Drawable) : Int{  
val bitmap = Bitmap.createBitmap(
textView?.width ?: 0,
textView?.height ?: 0,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
d.setBounds(0, 0, canvas.width, canvas.height)
d.draw(canvas)
return bitmap.getPixel(0, 0)
}

注意,我这里不考虑渐变色的情况,只是考虑单色的情况,所以x和y是传0,一般对于复杂的渐变色也不好做适配,但是对于background分边框和填充两种颜色的情况,一般文字都是显示在填充区域,这时候的x和y可以去根据边框宽度去加个偏移量(总之可以灵活应变)

还有一种场景,对于TextView没背景颜色,是它的父布局有背景颜色的情况,可以循环去调用父布局的view.background判断是否为空,为空就循环一次,不为空直接获取颜色。我这里就不演示代码了。

这里先把全部代码贴出来(都是用了最简单的方式)

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
kotlin复制代码override fun onCreate(savedInstanceState: Bundle?) {  
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_demo_text)

textView = findViewById(R.id.tv)
val d = textView?.background
textView?.post {
if (d != null){
if (isLightColor(getColorByDrawable(d))){
textView?.setTextColor(resources.getColor(R.color.black))
}else{
textView?.setTextColor(resources.getColor(R.color.white))
}
}
}
}

private fun getColorByDrawable(d : Drawable) : Int{
val bitmap = Bitmap.createBitmap(
textView?.width ?: 0,
textView?.height ?: 0,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
d.setBounds(0, 0, canvas.width, canvas.height)
d.draw(canvas)
return bitmap.getPixel(0, 0)
}

private fun isLightColor(color: Int): Boolean {
val r = color shr 16 and 0xFF
val g = color shr 8 and 0xFF
val b = color and 0xFF
val luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255
return luminance > 0.5
}

然后改几个背景色来看看效果

1
ini复制代码android:background="#000000"

image.png

1
ini复制代码android:background="#ffffff"

image.png

1
ini复制代码android:background="#3377ff"

image.png

1
ini复制代码android:background="#ee7700"

image.png

本文转载自: 掘金

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

PostgerSQL技术问答-00 Why Postgres

发表于 2024-04-26

笔者关于Nodejs的还有很多的坑还没有填上,这里又打算开一个新的坑系列-PostgreSQL,因为它们都是在Web应用开发中常用的技术。这里也不是写书,写博客就可以更随意一点。内容的碎片化不可避免,但只能先这样。可能等到时机成熟的时候,再重新组织整理一下。

这个系列的主题是:《PostgerSQL使用问答》,主要是总结针对在开发中遇到的一些PostgreSQL技术方面的疑问和思考,并且结合实例来进行分析和解答,在这个过程中,既解决了技术方面的问题,也提升了开发者的认知和水平。

本文作为系列文章的开篇,我们将会讨论一些和PostgreSQL数据库系统的背景相关的问题。笔者认为,这是除了技术本身之外,涉及到一个软件系统的价值观和方法论的基本问题,其实对于我们理解它是比较重要的。

什么是PostgreSQL

PostgreSQL的官方网站(下图)是: www.postgresql.org

0S.png

网站已经对于PostgreSQL,说得很清楚明白了。

PostgreSQL:

The World’s Most Advanced Open Source Relational Database

这句话里面,有三个要点:

  • 关系数据库系统

主要指关系数据模型和SQL语言的支持。说明PG是一个什么类型的软件和系统。

  • 开源软件

PG确实是开源软件。但笔者认为,PG的强大,更在于它拥有一个成熟稳定的开源技术社区,能够保证其作为开源软件系统的长期平稳发展,这其实比软件开源本身还重要。

  • 最先进

如果考虑的是开源关系数据库系统,它确实也名副其实。这主要体现在其强大而稳定的性能,对SQL完善的支持,丰富的特性,可扩展性等等。从这些角度来看,它也一点都不逊色于像Oracle、MS SQL Server这类主流的商业软件。

再说说这个产品名称是怎么来的。笔者在另一篇文章里面也提到过,PostgersSQL其实是三个单词(Post-gres-SQL)构成的:

  • gres: Ingres的简化(后详)
  • Post: 后Ingres的意思,意为这个软件发展来自Ingres
  • SQL: 使用SQL技术和语言的关系型数据库系统

简单而言,PostgerSQL的意思就是“支持SQL的后ingres数据库系统”。这其实简单的体现了这个产品的一个发展历史和过程。

Ingres的全称是Iteractive Graphics REtrieval System(交互式图形检索系统) 是始于1970年代早期加州大学伯克利分校的一个研究项目,在这个项目的研究基础上产生了关系型数据模型的理论和软件雏形;Postgres是其在80年代的后继项目,增加了类型定义和完整描述数据关系的能力;实际上Postgres计划在1993年已经终止;但是在1994年,有人在其BSD许可的基础上,利用其原始代码进行了后续开发,主要是加入SQL语言解释器,让它更像是一个标准的SQL数据库软件,命名为Postgres95,并在1996年重新命名为PostgreSQL,并作为开源软件系统,基于互联网进行分发,初始分发的版本就是PostgreSQL 6.0,可以视为PostgreSQL软件的正式开端。

为什么要选择PostgreSQL

单纯的讨论这个问题,或者和其他软件进行技术细节方面的比较,其实也没有太大的意义。因为实际上,任何一个软件,只要有人使用,就可能有它的独特价值和应用场景。没有软件天生就好或者烂,好的软件都是逐渐发展和完善起来的,关键是这个改善的过程能否平稳持续,并且能够与时俱进。

当然,选择PG肯定也是有一些理由的,笔者这里想从借用一张Hasso Plattner Institut制作的关系型数据库家谱开始:

RDBMS_Genealogy_V6.jpg

从这个家谱,我们应该可以感觉到,在所有的关系数据库系统分分合合,纵横交错,新旧变换的历程来看,PostgreSQL的发展一直是比较顺利稳定的。同时,作为开源软件系统,它也非常重视开源社区的建设和管理,这些都奠定了它在技术发展上,具有一定稳定性、适应性和前瞻性。

笔者感觉,和其他系统相比,PostgerSQL在技术上是经典的开源和互联网软件开发体系,这些特点和优势在于:

  • 专注于互联网应用开发的支持,和传统关系数据库系统专注于企业应用开发构成差异化
  • 发展策略是小幅改进、快速迭代、重视生态,贴近和适应互联网应用开发技术的发展
  • 理论先行,作为学院派软件,PG具有前瞻性的系统架构,严谨规范的架构设计,合理的软件和特性的生命周期管理
  • 功能丰富,易于扩展,这些都是快速多变的互联网应用开发所需要的,例如对于多种数据类型、数组、JSON等的支持,pgcrypto、uuid扩展等等
  • 跨平台支持,由于开源和基于C语言开发的属性,PostgreSQL很早就规划了对于多个主流操作系统平台的支持
  • 标准的关系型数据库特性:关系数据模型、高效的存储和索引引擎、ACID、SQL语言、网络服务等等
  • SQL标准规范的积极支持,并扩展出很多强大和实用的使用方式,如CTE、Returning、filter等等
  • 软件系统的稳定性,虽然没有数据方面的支撑,但从笔者的使用体验还有社区口碑而言,这个系统是非常稳定和可靠的,而且对于运维的要求,并没有想象那么高,还是非常易用和健壮的

这里额外提供一个有趣的信息,PostgreSQL的产品logo(下图),就是一头大象。可能开发者认为大象可以很好的代表PosgreSQL的产品特性和形象,就是强大、稳健和可靠。

OIP-C.jpg

Postgres有什么缺点或者不足

从技术和宏观上来看,几乎没有。因为关系型数据库系统技术发展到现在,已经是非常成熟和稳定的了。市场上存活下来这么几家,基本上可用性都非常好,不会有什么无法被接受的功能缺失或者缺点,有一些差异,几乎完全取义于对于场景和适应程度,开发体系的匹配以及开发者喜好。

但如果要较真的话,这里也可以尝试例举几条:

  • XID

XID就是TranactionID(事务ID),PG使用XID来对事务进行标识和管理。但PG设计的XID是一个32位的无符号整数。它有容量限制(2^32,40亿)。如果数据库的修改操作过于频繁或者数据过于庞大,可能会溢出并导致数据错乱的情况。这时需要运维人员进行关注和维护。

  • 数据表分区

老版本的PG,没有提供完善的数据表分区方案,需要一些额外的设置和操作。但这一问题已经在最近几个版本的迭代中基本上得到了解决,现在的数据表分区,功能和易用性已经基本和Oracle无异。

  • 群集和高可用

这一点可能相比Oracle略微逊色。PostgreSQL没有原生的群集和高可用性技术,但它也提供了基于日志的数据复制机制。一些社区和第三方开发者在此基础上开发了扩展的高可用集群系统方案。

  • 技术资源

笔者觉得,Postgres的社区是做得非常好的。但可能有人会认为它作为开源软件体系,没法提供和商业软件同一级别的服务。还有,特别是在中国,PG的应用还没有像欧美那样广泛,相关的技术、人才的资源相对还是缺乏的。

小结

本文作为PostgreSQL技术问答的开篇,简单的讨论了PostgreSQL的发展背景,和作为技术和产品的特点,让读者在正式进入系列之前,对这个技术有一个相对客观合理的理解和认知。

本文转载自: 掘金

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

一个纯策略游戏——Goofspiel

发表于 2024-04-26

上周接了一个小单,根据一个用户的需求用C++做了个小游戏——Goofspiel。游戏逻辑很简单,两个用户A,B和一个奖励方C,用户方每一轮出一张牌比点数,大的就赢得奖励牌。一共13轮,结束后,谁手中奖励牌之和大,谁就胜利。注意,用户牌和奖励牌的大小都是1到13。 有想体验的可以去试试别人写的网页版:coppercod.games/game/gops/

Goofspiel game

图1. Goofspiel game 网页版

一个不存在最优解的纯策略游戏

首先,这是一个不存在最优解的纯策略游戏。因为它是双方博弈性质的,不同对手有不同的出牌偏好,这也决定了你的出牌倾向。换言之,不同对手出牌的概率分布是变化的。

常见的游戏策略

这类游戏的策略非常多,我们列举几个如下:

  • 随机策略:随机从手中取出一张牌出
  • 确定性策略:假设奖励牌为prize_card,那么我们出一张确定的牌;即下次奖励牌为prize_card,我们扔出那张确定的牌
  • 加x策略:奖励牌为prize_card,那么我们出的牌就是(prize_card+x)%13
  • 拿中值牌策略:采用+3的方式,侧重于4,5,6,7,8,9,10这7张中值牌。对于非目标牌,可以随机性出牌

策略的评估

不同策略有不同的应对方法,下面列举一些策略的应对方法:

  • 对于随机策略,即出牌服从[1,13]的均匀分布。我们只需要出与奖励牌一样的牌,就可以获得最后的点数期望最大。
  • 对于确定性策略,如果你知道对手采用+x策略,我们只需要采用+(x+1)策略。

我们的策略

考虑到高价值奖励牌往往并不容易获取,我们对拿中值牌策略进行了改进。它可以自适应的预测对手出牌大小通过考虑轮次、奖励牌情况、对手历 史出牌和我们的手牌。这个策略针对下面三类牌会做出不同的反应:

  • 较低价值牌: 对于1到3的奖励牌x,如果是1,我们出手上最小的纸牌;否则我们倾向于选择一个[x, x+2]的随机卡牌。
  • 中等价值牌 (Prize cards 4-10): 对于4到10的奖励牌x,对局一开始,我们会选择+3的策略。然后,我们会动态学习对手拿牌的gap (gap= 出牌-奖励牌),从而针对性的拿牌。
  • 高价值牌 (Prize cards 11-13): 对于11到13的奖励牌x,我们根据对局状况来选择小牌、随机牌或者大牌。具体来讲,如果我们手上大牌比对手大牌大,则我们拿下该牌;如果我们手上大牌和对手大牌一样大,我们随机选择中等价值牌或者低价值牌;如果我们手上大牌没有对手大牌大,我们将随机选择小牌或者第三个四分位牌。
    同时,为了限制对手学习能力,一旦我们得分超过45分,我们将随机选择手牌。

设计相关算法

  • 随机出牌检测算法:我们的算法会分析对手历史出牌,从而判断对手是否是随机出牌。具体来讲,如果对手是随机出牌,当轮数足够大时,对于任意奖励牌x,对手牌将遵循区间在1到13的均匀分布。由于大数定律可知,样本均值收敛于数学期望7。我们从而可以判断对手是否是随机算法。
  • 确定性出牌检测算法:对于一类确定性+x的算法,我们提出了针对性的检测算法。通过检测(对手出牌-奖励牌)%13是否为常数,我们可以确定对手是否是确定性出牌。
  • 自适应出牌算法:该算法会帮助我们的策略拿下更多的中等价值牌。具体来看,一开始我们会以+3的方式冷启动,如果对手与我们竞争中等价值牌,我们会记录对手出牌与奖励牌的gap(gap=对手出牌-奖励牌)。下次面对中等价值牌时,我们会通过 gap+1方式出更有竞争力的牌。
  • 动态丢牌算法:该算法会帮助我们的策略合理的丢弃高价值牌。具体来看,对于一张高价值牌(11,12 或 13),如果我们手中的大牌没有对手大,我们会随机选择丢弃小牌或者中等价值牌(防止对手不出大牌)。如果我们和对手手中的大牌一样大,对于比赛初,我们会随机丢弃中等价值牌或最小牌;对于比赛后期,我们会出最大牌(后期弃牌会非常被动)。

运行截图

下面,我们对上述算法的实现结果进行展示:

image.png

图2.游戏主界面

image.png

图3. 自适应出牌

image.png

图4. 确定性拿牌与动态丢牌

image.png

图5. 游戏结果展示

代码

具体代码可去我的github查看,github.com/guchengzhon…

本文转载自: 掘金

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

【java GIUI】人机对弈五子棋

发表于 2024-04-26

在学校的Java课程中,我们被分配了一项有趣的任务:开发一款能够实现人机对弈的五子棋游戏。为了更好地理解Java GUI的运用,并与大家分享学习心得,我将整个开发过程记录在这篇博客中。欢迎大家阅读并提供宝贵的意见和建议!”

python版五子棋(讲解的更详细)

)编辑

1.绘制棋盘

)编辑

1.定义myPanel类。

myPanel相当于画板。

myPanel要继承 JPanel类,并要覆盖父类的paint方法,在paint方法里面写负责绘画的代码

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
java复制代码
public class myPanel extends JPanel {
static final int start = 6;
@Override
public void paint(Graphics g)
{
//调用父类的paint初始化画笔
super.paint(g);
//绘制背景
DrawBackground(g);
//绘制外边框
DrawBorder(g);
//绘制棋盘
DrawChessBoard(g);

}
public void DrawBackground(Graphics g)
{ //绘制背景
g.setColor(new Color(211, 152, 91));
g.fillRect(0, 0, 620,620);

}
public void DrawBorder(Graphics g)
{
//绘制外边框
g.setColor(Color.BLACK);
g.fillRect(5,5,610,5);
g.fillRect(610,5,5,610);
g.fillRect(5,610,610,5);
g.fillRect(5,5,5,610);
}

public void DrawChessBoard(Graphics g)
{
g.setColor(Color.BLACK);
//画横线
for (int i = 0; i < 19; i++) {
g.drawLine(6+i*32,6,6+i*32,614);
}
//画竖线
for (int i = 0; i < 19; i++) {
g.drawLine(6,6+i*32,614,6+i*32);
}

}
//画棋子
public void DrawChess(Graphics g,int x,int y,int type){
switch (type){
case 1:
g.setColor(Color.BLACK);
break;
case 2:
g.setColor(Color.WHITE);
}
g.fillOval(x*32+start,y*32+start,30,30);

}


}

2.定义myFrame类。

myFrame相当于窗口,画板要放在窗口里。

myFram要继承 JFram类,在初始化函数设置窗口参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码
public class MyFrame extends JFrame {
myPanel mp = null;
public MyFrame() {
mp = new myPanel();
this.setTitle("五子棋");
this.setSize(620, 620);
//添加画板
this.add(mp);
//点击窗口叉叉退出程序
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setVisible(true);
}

static public void main(String[] args) {
MyFrame mf = new MyFrame();
}
}

2.核心功能

1.实现下棋功能

1.定义相关变量

1
2
3
4
5
6
7
8
java复制代码    //偏移量
static final int IndexOffset = -10;
static final int ChessOffset = -10;
//黑子下棋标记
static boolean black = true;
boolean gameIsOver = false;

int chess[][]=new int[19][19];

2.添加事件监听

myPanel实现 MouseListener接口

重写mouseClicked方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码    @Override
public void mouseClicked(MouseEvent e) {
//计算棋子坐标
//# 0表示空棋
//# 1表示黑棋
//# 2表示白棋

int x = (e.getX() - IndexOffset) / 32;
int y = (e.getY() - 32 - IndexOffset) / 32;
System.out.println(y);
chess[x][y] = 1;
this.repaint();

}

myFrame添加事件监听

1
2
3
4
5
6
7
8
9
10
11
java复制代码    public MyFrame() {
mp = new myPanel();
this.setTitle("五子棋");
this.setSize(620, 620);
//添加画板
this.add(mp);
//点击窗口叉叉退出程序
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.addMouseListener(mp);
this.setVisible(true);
}

2.实现自动下棋

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
25
26
java复制代码
//# 0表示空棋
//# 1表示黑棋
//# 2表示白棋
//# 3表示下棋的位置




private static final int[][] cdata = {

{3, 1, 1, 1, 1}, {1, 3, 1, 1, 1}, {1, 1, 3, 1, 1}, {1, 1, 1, 3, 1}, {1, 1, 1, 1, 3},

{2, 2, 3, 2, 2}, {2, 2, 2, 3, 2}, {2, 3, 2, 2, 2}, {3, 2, 2, 2, 2}, {2, 2, 2, 2, 3},

{3, 1, 1, 1, 0}, {1, 1, 1, 3, 0}, {1, 1, 3, 1, 0}, {1, 3, 1, 1, 0},

{3, 2, 2, 2, 0}, {2, 3, 2, 2, 0}, {2, 2, 3, 2, 0}, {2, 2, 2, 3, 0},

{1, 1, 3, 3, 0}, {3, 1, 1, 0, 0}, {0, 1, 3, 1, 0},

{3, 2, 2, 0, 0}, {2, 2, 3, 0, 0}, {1, 3, 1, 0, 0},

{3, 1, 0, 0, 0}, {1, 3, 0, 0, 0}

};

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
java复制代码    public Point getBestPoint() {
//记录最低分(分值越低,优先级越高)
int score = 100;
//最佳位置
Point point = new Point(0, 0);
//每次都要遍历四个方向 左下 下 右下 右
int[] dx = {-1, 0, 1, 1};
int[] dy = {1, 1, 1, 0};
for (int i = 0; i < 19; i++) {

for (int j = 0; j < 19; j++) {
for (int k = 0; k < 4; k++) {
int cnt = 0;
for (int[] e : cdata) {
int m;
int x = i;
int y = j;
int bestX = 0;
int bestY = 0;
for (m = 0; m < 5; m++) {
if (e[m] == 3 && chess[x][y] == 0) {
bestX = x;
bestY = y;
} else {
if (chess[x][y] != e[m]) {
break;
}
}
x += dx[k];
y += dy[k];
if (x < 0 || x >= 19 || y < 0 || y >= 19) {
break;
}

}
if (m == 5) {
if (cnt < score) {
score = cnt;
point.x = bestX;
point.y = bestY;
}
break;
}
cnt++;
}
}

}
}
if (score < 100) {
return point;
} else {
int x = (int) (Math.random() * 19);
int y = (int) (Math.random() * 19);
while (chess[x][y] != 0) {
x = (int) (Math.random() * 19);
y = (int) (Math.random() * 19);
}
return new Point(x, y);
}

}

3.判断游戏是否结束

遍历棋盘

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
java复制代码   public boolean check() {
//每次都要遍历四个方向 左下 下 右下 右
int[] dx = {-1, 0, 1, 1};
int[] dy = {1, 1, 1, 0};
for (int i = 0; i < 19; i++) {
for (int j = 0; j < 19; j++) {
for (int k = 0; k < 4; k++) {
int x = i;
int y = j;
int m;
boolean flag = true;
for (m = 0; m < 4; m++) {
int tx = x + dx[k];
int ty = y + dy[k];
if (tx < 0 || tx > 19 || ty < 0 || ty > 19) {
flag = false;
break;
}
if (chess[x][y] != chess[x + dx[k]][y + dy[k]]) {
flag = false;
break;
} else if (chess[x][y] == 0) {
flag = false;
break;
}
x = tx;
y = ty;
}
if (flag) {
gameIsOver = true;
return true;
}

}
}
}
return false;
}

4.事件循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码   @Override
public void paint(Graphics g) {
//调用父类的paint初始化画笔
super.paint(g);
//绘制背景
DrawBackground(g);
//绘制外边框
DrawBorder(g);
//绘制棋盘
DrawChessBoard(g);
DrawChess(g);
//开始游戏

if (!gameIsOver)
game();

//游戏结束
if (check()) {
gameOver(g);
}

}

完整代码

myPanel

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
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
java复制代码import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

/**
*
*
* @author yuwei
* @date 23:29 2024/4/23
*/
public class myPanel extends JPanel implements MouseListener {
//偏移量
static final int IndexOffset = -10;
static final int ChessOffset = -10;
//黑子下棋标记
static boolean black = true;
boolean gameIsOver = false;

//# 0表示空棋
//# 1表示黑棋
//# 2表示白棋
//# 3表示下棋的位置


private static final int[][] cdata = {

{3, 1, 1, 1, 1}, {1, 3, 1, 1, 1}, {1, 1, 3, 1, 1}, {1, 1, 1, 3, 1}, {1, 1, 1, 1, 3},

{2, 2, 3, 2, 2}, {2, 2, 2, 3, 2}, {2, 3, 2, 2, 2}, {3, 2, 2, 2, 2}, {2, 2, 2, 2, 3},

{3, 1, 1, 1, 0}, {1, 1, 1, 3, 0}, {1, 1, 3, 1, 0}, {1, 3, 1, 1, 0},

{3, 2, 2, 2, 0}, {2, 3, 2, 2, 0}, {2, 2, 3, 2, 0}, {2, 2, 2, 3, 0},

{1, 1, 3, 3, 0}, {3, 1, 1, 0, 0}, {0, 1, 3, 1, 0},

{3, 2, 2, 0, 0}, {2, 2, 3, 0, 0}, {1, 3, 1, 0, 0},

{3, 1, 0, 0, 0}, {1, 3, 0, 0, 0}

};
int chess[][] = new int[20][20];

@Override
public void paint(Graphics g) {
//调用父类的paint初始化画笔
super.paint(g);
//绘制背景
DrawBackground(g);
//绘制外边框
DrawBorder(g);
//绘制棋盘
DrawChessBoard(g);
DrawChess(g);
//开始游戏

if (!gameIsOver)
game();

//游戏结束
if (check()) {
gameOver(g);
}

}

public void gameOver(Graphics g) {
Font font = new Font("Arial", Font.BOLD, 24);
g.setColor(Color.red);
g.setFont(font);
g.drawString("游戏结束", 270, 270);
}

public void DrawBackground(Graphics g) { //绘制背景
g.setColor(new Color(211, 152, 91));
g.fillRect(0, 0, 620, 620);

}

public void DrawBorder(Graphics g) {
//绘制外边框
g.setColor(Color.BLACK);
g.fillRect(5, 5, 610, 5);
g.fillRect(610, 5, 5, 610);
g.fillRect(5, 610, 610, 5);
g.fillRect(5, 5, 5, 610);
}

public void DrawChessBoard(Graphics g) {
g.setColor(Color.BLACK);
//画横线
for (int i = 0; i < 19; i++) {
g.drawLine(6 + i * 32, 6, 6 + i * 32, 614);
}
//画竖线
for (int i = 0; i < 19; i++) {
g.drawLine(6, 6 + i * 32, 614, 6 + i * 32);
}

}

//画棋子
public void DrawChess(Graphics g) {
for (int i = 0; i < 19; i++) {
for (int j = 0; j < 19; j++) {
if (chess[i][j] == 1) {
g.setColor(Color.BLACK);
} else if (chess[i][j] == 2) {
g.setColor(Color.WHITE);
} else {
continue;
}
g.fillOval(i * 32 + ChessOffset, j * 32 + ChessOffset, 30, 30);
}
}


}

public Point getBestPoint() {
//记录最低分(分值越低,优先级越高)
int score = 100;
//最佳位置
Point point = new Point(0, 0);
//每次都要遍历四个方向 左下 下 右下 右
int[] dx = {-1, 0, 1, 1};
int[] dy = {1, 1, 1, 0};
for (int i = 0; i < 19; i++) {

for (int j = 0; j < 19; j++) {
for (int k = 0; k < 4; k++) {
int cnt = 0;
for (int[] e : cdata) {
int m;
int x = i;
int y = j;
int bestX = 0;
int bestY = 0;
for (m = 0; m < 5; m++) {
if (e[m] == 3 && chess[x][y] == 0) {
bestX = x;
bestY = y;
} else {
if (chess[x][y] != e[m]) {
break;
}
}
x += dx[k];
y += dy[k];
if (x < 0 || x >= 19 || y < 0 || y >= 19) {
break;
}

}
if (m == 5) {
if (cnt < score) {
score = cnt;
point.x = bestX;
point.y = bestY;
}
break;
}
cnt++;
}
}

}
}
if (score < 100) {
return point;
} else {
int x = (int) (Math.random() * 19);
int y = (int) (Math.random() * 19);
while (chess[x][y] != 0) {
x = (int) (Math.random() * 19);
y = (int) (Math.random() * 19);
}
return new Point(x, y);
}

}

public boolean check() {
//每次都要遍历四个方向 左下 下 右下 右
int[] dx = {-1, 0, 1, 1};
int[] dy = {1, 1, 1, 0};
for (int i = 0; i < 19; i++) {
for (int j = 0; j < 19; j++) {
for (int k = 0; k < 4; k++) {
int x = i;
int y = j;
int m;
boolean flag = true;
for (m = 0; m < 4; m++) {
int tx = x + dx[k];
int ty = y + dy[k];
if (tx < 0 || tx > 19 || ty < 0 || ty > 19) {
flag = false;
break;
}
if (chess[x][y] != chess[x + dx[k]][y + dy[k]]) {
flag = false;
break;
} else if (chess[x][y] == 0) {
flag = false;
break;
}
x = tx;
y = ty;
}
if (flag) {
gameIsOver = true;
return true;
}

}
}
}
return false;
}

public void game() {

if (check()) {
return;
}
if (black) {
Point point = getBestPoint();
chess[point.x][point.y] = 1;
black = false;
this.repaint();
}


}


@Override
public void mouseClicked(MouseEvent e) {
//计算棋子坐标
//# 0表示空棋
//# 1表示黑棋
//# 2表示白棋
if (!black&&!gameIsOver) {
int x = (e.getX() - IndexOffset) / 32;
int y = (e.getY() - 32 - IndexOffset) / 32;
chess[x][y] = 2;
black = true;
this.repaint();

}

}

@Override
public void mousePressed(MouseEvent e) {

}

@Override
public void mouseReleased(MouseEvent e) {

}

@Override
public void mouseEntered(MouseEvent e) {

}

@Override
public void mouseExited(MouseEvent e) {

}
}

myFrame

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
java复制代码import javax.swing.*;

/**
* 用户服务
*
* @author yuwei
* @date 23:42 2024/4/23
*/
public class MyFrame extends JFrame {
myPanel mp = null;
public MyFrame() {
mp = new myPanel();
this.setTitle("五子棋");
this.setSize(620, 620);
//添加画板
this.add(mp);
//点击窗口叉叉退出程序
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.addMouseListener(mp);
this.setVisible(true);
}

static public void main(String[] args) {
MyFrame mf = new MyFrame();
}
}

感谢阅读,希望本文对你有所帮助

本文转载自: 掘金

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

Nestjs用装饰器从0到1实现HTTP Get请求

发表于 2024-04-26

前言

本文是基于小满zs的nest教学视频的个人学习笔记,大家感兴趣可以去看看原文
xiaoman.blog.csdn.net/article/det…

我们通过axios库发送http请求,首先安装axios

1
sh复制代码npm i axios -S

然后定义一个控制器 Controller

1
2
3
4
5
6
7
8
ts复制代码class Controller {
constructor(){

}
getList(){

}
}

现在我希望Controller的getList()方法可以获取后端API返回的list,同时通过装饰器@GET去发送http请求,getList()方法中不包含任何与网络请求有关的代码。

那么我们要做的第一步,将后端API的URL作为参数传入装饰器函数中,再由装饰器函数发起axios请求,按照正常人想法,既然装饰器本质是一个函数,那我能不能直接将URL作为参数,传入装饰器函数中呢?

1
2
3
4
5
6
7
8
9
10
11
12
ts复制代码const GET:MethodDecorator = (target, key, scriptor, URL:string)=>{
cosnole.log(URL)
}

class Controller{
constructor(){}

@GET("https://api.apiopen.top/api/getHaoKanVideo?page=0&size=10")
getList() {

}
}

image.png
可以发现代码报错,MethodDecorator函数类型的参数是固定的,不能随便添加。
因为TS严格约束数据类型,因此通过|来添加参数的方法并不可取,至少这种投机取巧的方式用的多了,项目便很难维护。所以现在,是高阶函数出场的时候了。

既然我们要将URL作为参数传入装饰器函数,但是装饰器函数不能接收新的参数。我们不妨在外面再套一层函数,最外面的一层函数接收URL,然后返回装饰器函数。由于装饰器函数引用了外层函数的URL,形成了一个闭包!我们的问题完美解决了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ts复制代码const GET = (URL: string):MethodDecorator=>{
return (target, key, scriptor)=>{
console.log(URL)
}
}

class Controller{
constructor(){}

@GET("https://api.apiopen.top/api/getHaoKanVideo?page=0&size=10")
getList() {

}
}

image.png

这种高阶函数就叫做装饰器工厂,装饰器的本质就是一个高阶函数。

接下来我们就可以继续编写装饰器函数中的逻辑:发送HTTP Get请求,然后将结果返回给装饰的getList()函数

这里我们复习一下方法装饰器函数的三个默认参数

  • 原型对象
  • 方法名称
  • 属性描述符
    • 可写:writable
    • 可枚举:enumerable
    • 可配置:configurable
    • ?value:对应的函数

所以我们可以通过descriptor属性描述符的value属性,获取到装饰的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ts复制代码import axios from "axios";

const GET = (url: string): MethodDecorator => {
return (target, key, descriptor) => {
//获取到装饰的函数
const fn = descriptor.value as Function;
//自定义参数status,用来传递状态码
let status = 0
//发送get请求
axios.get(url).then(res => {
//如果get请求发送成功,将返回的结果和其他自定义参数传递给getList函数
status = 200
fn(res, status)
}).catch(err => {
status = 500
fn(err, status)
})
}
}

然后我们使用@Get装饰getList方法,然后再getList函数里接收返回的参数。这句话是什么意思呢?我来解释给你听:
我们再axios发起Get请求后,调用了fn()函数,并往里面传递了一系列参数

1
2
3
4
5
6
7
8
ts复制代码        axios.get(url).then(res => {
//如果get请求发送成功,将返回的结果和其他自定义参数传递给getList函数
status = 200
fn(res, status)
}).catch(err => {
status = 500
fn(err, status)
})

而fn()函数是我们从方法装饰器的属性描述符中获取的

1
2
ts复制代码        //获取到装饰的函数
const fn = descriptor.value as Function;

结合起来就是,fn()函数接收到的参数,会传递给原方法,即被getList()函数接收

1
2
3
4
5
6
7
8
9
10
ts复制代码class Controller {
constructor() { }

@GET("https://api.apiopen.top/api/getHaoKanVideo?page=0&size=10")
//注意这里,我定义了两个参数,这两个参数就是从@GET装饰器传来的
getList(res: any, status: number) {
console.log(res.data)
console.log(status)
}
}

这样通过装饰器实现了,将发送HTTP Get请求的逻辑全部集中到@GET返回的装饰器函数,将Get请求返回的结果处理逻辑,全部集中到了getlist()方法中。

总结

我们通过定义装饰器工厂函数(高阶函数),解决了向装饰器传递自定义参数的问题
通过装饰器,成功将发送Get请求逻辑和处理Get请求逻辑分开

本文转载自: 掘金

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

Docker入门到精通《Nexus的搭建与使用》

发表于 2024-04-26

一、什么是私有镜像仓库?

镜像仓库(Docker Registry)有公有和私有的两种形式:

  • 公共仓库:例如Docker官方的Docker Hub,国内也有一些云服务商提供类似于Docker Hub的公开服务,比如网易云镜像服务、DaoCloud镜像服务、阿里云镜像服务等。
  • 除了使用公开仓库外,用户还可以在本地搭建私有Docker Registry。企业自己的镜像最好是采用私有Docker Registry来实现。比如Nexus 、Harbor 、等。

二、私有仓库能做什么?

  • 可以存储公司私有镜像,避免暴露到外网。
  • 快速交付:当应用程序开发完成后,可以直接打包成一个镜像,将镜像上传到私有仓库后,可以在任何装有Doker的机器上下载该镜像,并运行为一个容器。

image.png

三、Nexus 、Harbor的区别

  • Harbor:可以自建文件夹进行分组这点就非常好。其实说实话,作为一个私有的镜像仓库,Harbor已经做得很好的了,唯一的缺点是它无法帮你下载镜像。因为在kubernetes环境下,你肯定有去公网拉镜像的需求,无论是官方还是非官方。你不可能因为这个而特地给你的所有node节点开通外网访问吧,这样风险太多且不可控。在我看来,整个kubernetes集群都不能访问外网。
  • Nexus :当你需要拉公网镜像的时候,你只要向它发起请求,它如果本地没有,就会自动去你配置的镜像仓库下载,下载完成之后先在本地缓存一份,然后发送给你。你甚至不用给Nexus开通外网,只需要在一台可以访问外网的机器上搭建一个代理服务就行,让Nexus通过代理去下载。

四、Nexus的搭建

4.1 Docker安装Nexus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
yaml复制代码# Step 1:保证服务器对Nexus端口开放

# Step 2:创建/opt/docker/nexus文件夹。
# -p: 没有就创建。
mkdir -p /opt/docker/nexus

# Step 3:放给最高权限,方便使用
chmod 777 -R /opt/docker

# Step 4:运行 nexus3 容器
docker run -d --restart=always -p 8081:8081 --name nexus_container -v /opt/docker/nexus:/nexus-data sonatype/nexus3

# Step 5:日志查看
docker logs -f nexus_container

五、Nexus的使用

5.1 登录Nexus

1
2
3
4
5
6
7
yaml复制代码# Step 1:安装完成后可访问Nexus管理平台:http:ip:端口

# Step 2:登录Nexus,默认管理员用户名:admin 密码:admin123,如果提示密码不对,需要到容器里面查看管理员admin密码。
# cat /home/nexus/data/admin.password


# Step 3:第一次登陆之后,一般提示修改密码!修改密码之后,重新登录!

image.png

image.png

image.png

5.2 配置Nexus

Nexus默认创建了几个仓库,如下:
image.png

5.2.1 仓库名称

仓库名称 说明
maven-central Nexus 对 Maven 中央仓库的代理,默认从repo1.maven.org/maven2/拉取ja…
maven-releasse Nexus 默认创建,供开发人员部署自己 jar 包的宿主仓库,私库发行版jar,初次安装请将Deployment policy设置为Allow redeploy要求 releasse 版本
maven-snapshots Nexus 默认创建,供开发人员部署自己 jar 包的宿主仓库,要求 snapshots 版本
maven-public Nexus 默认创建,仓库分组,把上面三个仓库组合在一起对外提供服务,在本地maven基础配置settings.xml或项目pom.xml中使用

5.2.2 仓库类型

仓库类型 说明
proxy 代理仓库:它们被用来代理远程的公共仓库,如maven中央仓库。
hosted 本地仓库:通常我们会部署自己的构件到这一类型的仓库。比如公司的第二方库。
group 仓库组:用来合并多个hosted/proxy仓库,当你的项目希望在多个repository使用资源时就不需要多次引用了,只需要引用一个group即可。

5.3 创建 Nexus仓库

除了自带的仓库,有时候我们需要单独创建自己的仓库,按照默认创建的仓库类型来创建我们自己的仓库。点击Create Repository
Snipaste_2023-10-10_19-02-20.png

选择如下三种类型的仓库。
Snipaste_2023-10-10_19-02-41.png

5.3.1 Hosted 仓库

输入仓库名,点击创建即可。
Snipaste_2023-10-10_19-03-43.png

5.3.2 Proxy仓库

输入仓库名以及被代理仓库的URL,这里我输入阿里云的仓库地址,默认为中央仓库。
Snipaste_2023-10-10_19-07-00.png

5.3.3 Group仓库

根据group仓库的解释,group仓库是其他仓库的集合,所以需要将其他创建的仓库添加到组里。
Snipaste_2023-10-10_19-07-45.png

上面的仓库创建好之后就可以在首页看到了。
Snipaste_2023-10-10_19-08-17.png

5.4 通过 Nexus 下载 jar 包

5.4.1 创建一个新仓库

Nexus私服搭建好之后就可以通过Nexus下载jar包了。为了方便演示我在本地创建了一个空的仓库:new-repo
Snipaste_2023-10-10_19-10-43.png

5.4.2 修改Maven的配置,将新仓库作为默认仓库

Snipaste_2023-10-10_19-11-26.png
Snipaste_2023-10-10_19-11-45.png

5.4.3 修改镜像配置(这里我们之前都是配置的阿里云仓库,现在改为我们自己的Nexus仓库)

Snipaste_2023-10-10_19-16-53.png

1
2
3
4
5
6
xml复制代码     <mirror>
<id>maven-public</id>
<mirrorOf>central</mirrorOf>
<name>Maven public</name>
<url>http://192.168.11.164:8090/repository/maven-public/</url>
</mirror>

这里的 url 标签是这么来的:
Snipaste_2023-10-10_19-14-26.png
把上图中看到的地址复制出来即可。如果我们在前面允许了匿名访问,到这里就够了。但如果我们禁用了匿名访问,那么接下来我们还要继续配置settings.xml:
Snipaste_2023-10-10_19-17-08.png

1
2
3
4
5
xml复制代码  <server>
<id>maven-public</id>
<username>admin</username>
<password>admin</password>
</server>

注意:server 标签内的 id 标签值必须和 mirror 标签中的 id 值一样,用户名和密码是修改之后的。

5.4.4 验证

我们新建一个Maven项目然后引入一个依赖(这里我用fastjson2举例)来验证jar包是否是通过Nexus下载的。新建项目之后修改项目的Maven配置以及引入依赖。

Snipaste_2023-10-10_19-19-18.png

Snipaste_2023-10-10_19-20-32.png

Snipaste_2023-10-10_19-22-08.png

等待下载完成我们刷新对应的仓库可以发现jar包已经下载到Nexus里面了。
Snipaste_2023-10-10_19-28-29.png

5.5 将 jar 包部署到 Nexus

演示完通过Nexus下载jar包,接下该演示怎么将本地模块打包发布到Nexus私服,让其他的项目来引用,以更简洁高效的方式来实现复用和管理。

因为发布jar包涉及到snapshots和releases仓库,所以需要配置这两个的仓库的访问权限,同样的这是针对禁用了匿名访问的操作的,如果没有禁用匿名访问,这里依然不用配置。

5.5.1 需要配置的server:

Snipaste_2023-10-10_19-29-47.png
Snipaste_2023-10-10_19-33-08.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<server>
<id>maven-public</id>
<username>admin</username>
<password>admin</password>
</server>
<server>
<id>maven-releases</id>
<username>admin</username>
<password>admin</password>
</server>
<server>
<id>maven-snapshots</id>
<username>admin</username>
<password>admin</password>
</server>

5.5.2 配置pom.xml

然后在我们需要上传的 maven 项目中的pom.xml添加如下配置:
Snipaste_2023-10-10_19-33-58.png

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<!-- 这里的 id 要和上面的 server 的 id 保持一致,name 随意写-->
<distributionManagement>
<repository>
<id>maven-releases</id>
<name>Releases Repository</name>
<url>http://192.168.11.164:8090/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>maven-snapshots</id>
<name>Snapshot Repository</name>
<url>http://192.168.11.164:8090/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>

5.5.3 部署到Nexus

然后点击部署deploy就可以将项目发布到Nexus了。
Snipaste_2023-10-11_10-28-23.png

然后刷新maven-snapshots仓库就可以发现,项目已经发布了。
Snipaste_2023-10-11_10-29-17.png

5.6 疑问

注意:为什么是发布到snapshot仓库呢?那如果想要发布到releases仓库该怎么做呢?

这是因为我们创建Maven的时候,版本号默认为1.0-SNAPSHOT,所以就对应发布到snapshot仓库了,只需要将版本号改为正式版本号就可以了。
Snipaste_2023-10-11_10-30-02.png

然后点击deploy就可以在releases仓库找到了。
Snipaste_2023-10-11_10-30-31.png

最后,介绍一下Maven几个常用命令的作用。

package 命令完成了项目编译、单元测试、打包功能。

install 命令完成了项目编译、单元测试、打包功能,同时把打好的可执行jar包(war包或其它形式的包)布署到本地maven仓库。

deploy 命令完成了项目编译、单元测试、打包功能,同时把打好的可执行jar包(war包或其它形式的包)布署到本地maven仓库和远程maven私服仓库。

本文转载自: 掘金

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

AI菜鸟向前飞 — LangChain系列之七 - 深入浅出

发表于 2024-04-26

开篇

EL简单实现,原理初探

AI菜鸟向前飞 — LangChain系列之六 - 深入浅出LCEL与Chain(上篇)

上一篇文档给大家介绍的Expression Language 特别提到了,


每个运行对象(即:Runnable)通过"|"(管道)连接组成了Chain,可以通过更快速书写且更易读的方式带来了很好的体验效果,在文章中的最后,我给大家展示了每一个Runnable对象的输入/输出Schema,可能有些小伙伴还是有点懵,这样我通过一个简单的实例,自己来实现一个`Pipeline`

程序实现

1
2
3
4
5
6
7
8
9
10
11
12
13
ruby复制代码class MyPipeline:
def __init__(self, func):
self.func = func
def __or__(self, otherfunc):
def _func(*args, **kwargs):
return otherfunc(self.func(*args, **kwargs))
return MyPipeline(_func)

def __call__(self, *args, **kwargs) :
return self.func(*args, **kwargs)

def invoke(self, *args, **kwargs):
return self.__call__(*args, **kwargs)
再准备一下
1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码def hello(name) -> str:
return f"hello,{name}。"

def welcome(greeting) -> str:
return f"欢迎来到我的公众号, {greeting}"

def attach_date(mstr) -> str:
return f"{mstr} 现在是{datetime.now().strftime("%Y-%m-%d")}。"

# 为什么这里要写这么麻烦,后面介绍RunnableLambda你就懂了
hello = MyPipeline(hello)
welcome = MyPipeline(welcome)
attach_date = MyPipeline(attach_date)

试出效果

1
2
3
scss复制代码# my_chain = mychain = hello.__or__(welcome).__or__(attach_date)
my_chain = hello | welcome | attach_date
print(test.invoke("Song榆钱儿"))

输出结果

  • 1
1
复制代码欢迎来到我的公众号, hello,Song榆钱儿。现在是2024-04-26。

分析过程

以图代言 —— 可以直接看懂通过管道(“|”)让可运行对象(Runnable)之间如何传递数据的。(这样也更好理解上一篇最后的内容)

图片

简单解析

1
2
arduino复制代码这里主要用到了Python的魔法函数__or__,所以"|"这里对于Python实现还是比较简单的,若大家对此感兴趣,可以持续关注我哈
同时,我们也可以看到LangChain底层也是通过这种“方式”实现的

图片

1
复制代码

图片

思考题

1
2
ini复制代码# LangChain还支持这种形式pipe连接多个Runnable对象,你可以自行实现下:)
my_chain = hello.pipe(welcome).pipe(attach_date)

LangChain的其它相关函数

Runnable对象支持的函数比较多,如下所示:官网API地址: 


[api.python.langchain.com/en/stable/r…](https://api.python.langchain.com/en/stable/runnables/langchain_core.runnables.base.Runnable.html#langchain_core.runnables.base.Runnable)

这里挑比较有代表性的三个给大家介绍,

RunnableLambda

让我们再回到LangChain中,看看RunnableLambda是怎么用的

图片

RunnableLambda能将普通函数转换为Runnable对象,并使用EL(Expression Language)语法

with_fallbacks

程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码from langchain_openai import ChatOpenAI
from langchain_community.chat_models import ChatOllama
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

prompt = ChatPromptTemplate.from_template("{text}")
# 我的OpenAI Key已经过期,调用它肯定报错
model_openai = ChatOpenAI()
# 使用我本地启动的Ollama llama3大语言模型,肯定没问题
model = ChatOllama(model="llama3", temperature=0)

output_parser = StrOutputParser()
chain = prompt | model_openai.with_fallbacks([model]) | output_parser
response = chain.invoke({"text": "你好啊, AI小助手"})

print(response)

输出结果

图片

分析过程

图片

Bind

以后Tool相关知识的会用到它


程序&输出结果

图片

不加bind(stop...)呈现的效果如下:

图片

期待下篇吧~ ㊗️大家周末愉快

本文转载自: 掘金

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

1…91011…956

开发者博客

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