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

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


  • 首页

  • 归档

  • 搜索

链式思维提示是什么?Prompt 加上这一句就能让 AI 像

发表于 2024-04-25

文章目录

  • 简介
  • 链式思维提示的概念
  • 链式思维提示的优势
  • 链式思维提示的局限性
  • 链式思维提示与提示语链的对比
  • 总结

cover-2.png

简介

在 AI 领域,尤其是在自然语言处理的子领域中,链式思维提示 (Chain of Thought prompting) 已经成为了一种重要的工具。它不仅提高了大型语言模型(LLM)的逻辑推理能力,而且还提升了模型在处理复杂任务时的准确性和效率。

本文将从 CoT 的概念、优势、局限性以及与提示语链的对比多个方面,并结合一些具体的案例来详细讲解链式思维提示。

链式思维提示的概念

我们在上学的时候,都做过数学题。无论是考试也好,还是日常做题也好,老师教给我们的习惯是,先写「解」字,然后分析题目,随后一步一步地推理和解答问题,最后得出答案。这个思维方式,也同样可以应用在 AI 上。

链式思维提示 (Chain of Thought prompting) 作为一种提示语工程(Prompt Engineering)技术,其核心在于通过精心构建输入提示,以模仿人类的推理过程,从而提升语言模型在处理逻辑、计算和决策任务时的性能和效率,也让模型输出的内容更直观易懂。

CoT.png

为了构建有效的链式思维提示,我们通常会在查询的末尾附加一条指令,比如 请描述你的推理步骤 或 请逐步解释你的答案,这样可以引导 LLM 进行深入的思考。这种方法要求 LLM 不能只是给出最终结果,还必须要详细阐述是如何形成这样的答案的,让 LLM 将中间步骤一并地回答出来。

Google Brain 研究团队有一篇论文展示了关于链式思维提示在多项算术、常识和符号推理的基准测试中,相较于标准提示技术,展现出了更优越的性能。

We explore how generating a chain of thought – a series of intermediate reasoning steps – significantly improves the ability of large language models to perform complex reasoning. In particular, we show how such reasoning abilities emerge naturally in sufficiently large language models via a simple method called chain of thought prompting, where a few chain of thought demonstrations are provided as exemplars in prompting. Experiments on three large language models show that chain of thought prompting improves performance on a range of arithmetic, commonsense, and symbolic reasoning tasks. The empirical gains can be striking. For instance, prompting a 540B-parameter language model with just eight chain of thought exemplars achieves state of the art accuracy on the GSM8K benchmark of math word problems, surpassing even finetuned GPT-3 with a verifier.

我们探索了生成思维链(一系列中间推理步骤)如何显著提升大型语言模型执行复杂推理的能力。具体而言,我们展示了一种名为“思维链提示”的简单方法,可以使足够大的语言模型自然地产生这种推理能力。这种方法通过在提示中提供少量思维链示例作为样例来实现。在三个大型语言模型上的实验表明,思维链提示可以在一系列算术、常识和符号推理任务上提高性能。 这种方法带来的性能提升是显著的。例如,使用仅有八个思维链示例对一个 540B 参数的语言模型进行提示,就可以在数学应用题基准测试 GSM8K 上实现最先进的精度,甚至超过了经过微调并带有验证器的 GPT-3 模型。(由 Google Gemini 翻译,论文链接可在文章底部找到)

无论是在实际生活还是在我们解决各类问题的场景下,当我们面对复杂问题时,我们倾向于将其分解为更小、更易管理的部分。比如,解决复杂的数学方程往往就会涉及多个子步骤,每个步骤都是用来推导出正确答案的关键。因此,链式思维提示要求 LLM 模仿这种分解问题并逐步解决问题的过程,本质上是要求模型 大声思考,而不是只提供一个直接的解决方案。

河流穿越逻辑题

我们来看一个案例:当用户向 LLM 提出一个经典的河流穿越逻辑谜题,并要求 逐步描述你的推理 时,模型会逐步解决问题,描述每次过河的过程,直至找到最终的解决方案。

提示语

一个男孩和两只绵羊、一头狼一起旅行,他们需要坐船穿越一条河流,但只有两只船可用,并且每只船都很小,只能容下一个人和一只小动物。如果单独留下两只绵羊中的其中一只,饿狼就会吃掉他们。要怎么样才能保证这个小男孩和绵羊、狼一起安全到达河对岸呢?
请逐步描述你的推理过程。

回答内容

以下是小男孩和两只绵羊、一头狼安全到达河对岸的步骤:

  1. 第一步:男孩将一只绵羊带到河对岸。

男孩先将一只绵羊带到河对岸,留下狼和另一只绵羊在河边。

  1. 第二步:男孩独自返回河边。

男孩独自返回河边,没有带任何动物。

  1. 第三步:男孩将另一只绵羊带到河对岸。

男孩将另一只绵羊带到河对岸,留下狼在河边。

  1. 第四步:男孩将狼带到河对岸。

男孩将狼带到河对岸,留下第一只绵羊在河边。

  1. 第五步:男孩返回河边,将第一只绵羊带到河对岸。

男孩返回河边,将第一只绵羊带到河对岸,完成所有动物的运送。

推理过程:

  • 关键在于男孩不能单独留下狼和任何一只绵羊,否则狼会吃掉绵羊。
  • 因此,男孩需要每次都带一只动物一起过河,并确保另一只动物的安全。
  • 在第一步和第三步,男孩将两只绵羊分别运送到河对岸,但每次都需要独自返回河边。
  • 在第二步和第四步,男孩将狼运送到河对岸,但需要将另一只绵羊留在河边。
  • 最后一步,男孩返回河边,将第一只绵羊带到河对岸,完成所有动物的运送。

这个方法利用了男孩和狼之间的时间差,确保了所有动物的安全。

这里还有一些其他的链式思维提示例子,它们展示了如何通过逐步推理来解决问题,你可以尝试着讲这些提示语放到对应的 LLM 中去,看看它会给出什么样的回答:

  • 我有一块圆形蛋糕,切成了八等份。小黄吃了三片,他的朋友吃了两片。还剩下多少片?请逐步解释你的推理。
  • 阿润在冬天的某个晚上把一杯水放在外面,室温是零下 20 度左右。第二天早上,他发现玻璃杯裂了。请逐步解释玻璃杯为什么会裂。
  • 如果所有的玫瑰都是花,同时,又由于有些花很快就会凋谢,我们能否得出结论说有些玫瑰很快就会凋谢?请逐步解释你的推理。
  • 一个教室里每三把红椅子就有两把蓝椅子。如果教室里总共有 30 把椅子,那么有多少把蓝椅子?逐步描述你的推理。

链式思维提示的优势

LLM 在其庞大的文本数据集上的训练过程中,接触到了大量的解释、定义和解决问题的例子,这些数据集包括书籍、文章和大部分开放的互联网信息。链式思维提示通过触发模型调用和应用这些信息,这样就可以从这个巨大的知识库中获取到有效的信息。

然而,我们依然必须认识到 LLM 在逻辑推理方面的局限性。尽管 LLM 擅长生成连贯、相关的文本(其实都还并不算完美,胡说八道不相关的内容的情况数不胜数),但它们目前在提供事实信息或解决问题方面,确实还差强人意上,它们会经常遇到困难,尤其是在更复杂的问题上。

链式思维提示的优势在于,它允许 LLM 一次只处理有限的信息量,将复杂问题分解成更简单的子问题,从而来提高模型响应的准确性和精确性。链式思维提示可以通过引导模型采取结构化的推理方法来解决这一限制,这样就有助于减少逻辑失误和疏忽的可能性。

另外一个重要的优势就是,链式思维提示可以通过使模型得出答案的过程更加透明,帮助模型调试和改进。因为链式思维提示要求模型明确地描述一个推理过程,它们为模型测试者和开发者提供了更好的洞察力,了解模型是如何得出特定结论的。这反过来可以使在完善模型时更容易识别和纠正错误。

在未来的工作中,将链式思维提示与微调结合起来,可以增强 LLM 的推理能力。例如,在一个包含逐步推理和逻辑演绎的精选例子的训练数据集上对模型进行微调,可以进一步提高链式思维提示的有效性。

链式思维提示的局限性

重要的是,正如 Google Brain 研究团队在他们的论文中强调的,链式思维提示从 LLM 中引出的类似推理的表现,并不意味着模型实际上在思考。使用 LLM 时,必须记住,模型是一个基于概率预测文本序列的神经网络,目前并没有证据表明 LLM 能像人类一样进行推理。这一区别对于我们解 LLM 的局限性和保持对其能力的现实期望至关重要。

LLM 缺乏意识和认知反思,它们的通用知识完全来自于训练数据,因此它们给出的内容很有可能会反映出该数据集的错误、缺陷和偏见。

尽管 LLM 可以准确地模仿逻辑推理的结构,但并不一定意味着其结论本身就是准确的。链式思维提示确实可以作为 LLM 输出的有价值的一个有效 Prompt 机制,LLM 可能呈现出连贯、结构良好的输出,但是其中可能会包含逻辑错误和疏漏,用一句我们中学老师的话说就是:按照正确的计算过程得出错误的答案。

当然,也不是说就没有更好的方案。像诸如检索增强生成(RAG)之类的技术有可能会在一定程度上解决问题。RAG 可以使 LLM 能够在实时请求提供事实信息时,访问外部来源的数据,比如经过审核的数据库或互联网上的信息。这样一来,RAG 就消除了大家对于 LLM 仅依赖其训练数据中获得的内部知识库的顾虑,毕竟,这些知识库可能存在缺陷或不完整。

然而,就算 RAG 可以提高 LLM 输出的准确性和时效性,但它并没有从根本上解决逻辑推理的问题。推理和逻辑推导不仅仅需要事实回忆,还涉及通过逻辑和分析得出结论的能力,这些方面的 AI 性能更密切地与 LLM 本身的算法架构和训练有关。

此外,链式思维提示的可扩展性仍然是一个问题。尽管顺序、逐步推理的基本原则在 AI 和机器学习中广泛适用,但由于思维连提示依赖于 LLM 在语言任务上的复杂表现,所以这项技术目前仅限于 LLM。

LLM 庞大的规模需要大量的数据、计算和基础设施,这会引发对于可访问性、效率和可持续性的问题。为了应对这些问题,现在市面上已经有部分小规模的 LLM 发布了出来(比如 Meta 的 Llama 3),虽然它们不如那些大型的 LLM 强大,但在各种细分领域上的表现极具竞争力,它们的最大优势就是只需要较少的计算资源。然而,链式思维提示的优势是否能完全转移到较小的模型上还有待观察,因为减少它们的能力可能会损害它们解决问题的有效性。

meta.png

最后也是最重要的一点,链式思维提示是一种更有效使用现有模型的技术,而不是一种训练方法。虽然链式思维提示可以帮助我们从预训练的 LLM 中获得更好的结果,但 Prompt Engineering 也不是万能的,并不能解决那些应该在训练阶段就该处理的模型限制。

链式思维提示与提示语链的比较

虽然链式思维提示和提示语链听起来相似,而且都是 Prompt Engineering 技术,但它们在一些重要方面也有所不同。

正如上文中讲到的那样,链式思维提示要求模型描述用于推理出最终答案的中间步骤,这一过程包含在一个回答中。这对于需要详细解释、规划和推理的问题非常有用,例如数学问题和逻辑谜题,解释思考过程有助于我们理解问题的解决方案。

相比之下,提示链涉及一系列迭代的提示语和回答,其中每个后续的提示语都是基于模型对前一个提示语的输出来制定的。这使得提示语链成为更具创造性、探索性任务的有用技术,这些任务涉及逐步细化,如生成详细叙述和头脑风暴想法。

链式思维提示和提示语链的根本区别在于迭代和互动性。链式思维提示旨在将推理过程封装在一个详细的、自包含的回答中。而提示语链则采取了更为动态的方法,通过多轮互动,让一个想法随着时间的推移而发展。这种迭代过程不仅促进了思想的深入发展,而且还为创新提供了更广阔的空间。

总结

总结而言,链式思维提示和提示链都是强大的 Prompt Engineering 技术,它们通过不同的方式增强了 LLM 的能力。链式思维提示通过详细的推理步骤提高了透明度和理解性,而提示链则通过迭代互动促进了创意的发展。

选择哪种技术取决于特定任务的需求和目标。无论选择哪种方法,都要记住,这些技术都是工具,最终效果取决于你如何使用它们,正确应用这些技术可以显著提高 LLM 的性能和准确性,但也需要你对模型的局限性有清晰的认识。

本文转载自: 掘金

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

安卓开发 Jetpack Compose 的状态管理

发表于 2024-04-25

为什么会有状态管理

因为UI界面并不是一成不变的,所以需要在数据状态发生变化时更新界面,管理界面中的状态。

Flutter 的状态管理

在Flutter中有很多优秀的状态管理的库,比如Getx,provider ,bloc 等,基于Stream流或者 Flutter自带内置组件InheritedWidget来封装,不管是什么框架状态管理本质都是 发布-订阅模式。之前写过一篇关于Flutter的状态管理 Flutter 状态管理的学习

Android 中状态管理

完成一个简单的ToList

添加或删除 todo,完成todo项

让组件有状态

使用 mutableStateOf ,
by关键字表示这是一个委托属性。

1
kotlin复制代码var tasks by remember { mutableStateOf(dummyTasks) }

委托属性

当你访问 tasks 属性时,实际上是委托给了 mutableStateOf(dummyTasks) 创建的状态对象,读取其中 的值。当你给 tasks 赋新值时,也是委托给了同一个状态对象,对其进行了修改。使用委托属性的好处是,你可 以将属性的底层实现委托给另一个对象,而不需要自己手动实现 getter 和 setter 方法。

remember 和 rememberSaveable

虽然 remember 可在重组后保持状态,只会记住重组后的,执行任何导致 Android 重新创建运行中 activity 的其他配置更改时,状态还会重置。所以横竖屏的切换,状态就会重置。要想更改配置状态依旧保留需要使用 rememberSaveable,因为rememberSaveable会自动保存可保存在 Bundle中的任何值中,

使用 remember 存储对象的可组合函数包含内部状态,这会使该可组合函数有状态。

让列表成为可变列表

使用可变 ArrayList<T> 或 mutableListOf,。这些类型不会通知列表中的项已发生更改并安排界面重组。
从列表中添加或删除任务的行为,第一步是让列表成为可变列表,创建一个可由 Compose 观的 MutableList 实例。允许 Compose 跟踪更改,以便在列表中添加或移除项时重组界面。

1
2
3
4
5
kotlin复制代码val dummyTasks = mutableListOf(
Task(id = 1, title = "买菜"),
Task(id = 2, title = "做饭"),
Task(id = 3, title = "洗衣服")
)
mutableStateListOf 和 toMutableStateList

mutableStateOf 函数会返回一个类型为 MutableState<T> 的对象。

mutableStateListOf 和 toMutableStateList 函数会返回一个类型为 SnapshotStateList<T>

mutableStateListOf 不能被委托

mutableStateListOf 不能被委托的原因,委托属性需要实现 getValue() 和 setValue() 方法。
而 mutableStateListOf本身并没有实现这两个方法,因此无法作为委托属性使用。

LiveData和ViewModel:

在官方文档的介绍中LiveData和ViewModel 都是生命周期感知型组件

ViewModel

官方简介:*是一种业务逻辑或屏幕级状态容器。它用于将状态公开给界面,以及封装相关的业务逻辑。 它的主要优点是,它可以缓存状态,并可在配置更改后持久保留相应状态。
*

优势 :

ViewModel 类的主要优势实际上有两个方面:

  • 它允许持久保留界面状态。
  • 它可以提供对业务逻辑的访问权限。

ViewModel 的生命周期

  1. 创建阶段(onCreate): 当Activity或Fragment首次创建时,ViewModel被创建并初始化。这是ViewModel的生命周期的开始阶段。
  2. 活动阶段(active): 一旦ViewModel被创建并与Activity或Fragment相关联,它会进入活动阶段。在这个阶段,ViewModel可以被观察者观察,并且可以存储和管理与UI相关的数据。
  3. 销毁阶段(onCleared): 当Activity或Fragment被销毁时,ViewModel的onCleared()方法会被调用。在这个方法中,你可以执行一些清理工作,例如取消异步任务或释放资源。

ViewModel的生命周期超出了配置更改引起的Activity或Fragment的销毁和重新创建。这意味着,即使Activity或Fragment被销毁和重新创建,ViewModel仍然存在,并且可以保持其状态和数据。这样可以确保在配置更改后,例如屏幕旋转,用户切换了应用的语言,或者由于系统内存不足导致的重新创建,数据不会丢失,UI状态得以保持。

LiveData

官方定义: 是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循其他应用组件(如 activity、fragment 或 service)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。

如果观察者(由 Observer 类表示)的生命周期处于 STARTED 或 RESUMED 状态,则 LiveData 会认为该观察者处于活跃状态。LiveData 只会将更新通知给活跃的观察者。为观察 LiveData 对象而注册的非活跃观察者不会收到更改通知。

使用 LiveData 的优势

使用 LiveData 具有以下优势:

  • 确保界面符合数据状态

LiveData 遵循观察者模式。当底层数据发生变化时,LiveData 会通知 Observer 对象。您可以整合代码以在这些 Observer 对象中更新界面。这样一来,您无需在每次应用数据发生变化时更新界面,因为观察者会替您完成更新。

  • 不会发生内存泄漏

观察者会绑定到 Lifecycle 对象,并在其关联的生命周期遭到销毁后进行自我清理。

  • 不会因 Activity 停止而导致崩溃

如果观察者的生命周期处于非活跃状态(如返回堆栈中的 activity),它便不会接收任何 LiveData 事件。

  • 不再需要手动处理生命周期

界面组件只是观察相关数据,不会停止或恢复观察。LiveData 将自动管理所有这些操作,因为它在观察时可以感知相关的生命周期状态变化。

  • 数据始终保持最新状态

如果生命周期变为非活跃状态,它会在再次变为活跃状态时接收最新的数据。例如,曾经在后台的 Activity 会在返回前台后立即接收最新的数据。

  • 适当的配置更改

如果由于配置更改(如设备旋转)而重新创建了 activity 或 fragment,它会立即接收最新的可用数据。

  • 共享资源

您可以使用单例模式扩展 LiveData 对象以封装系统服务,以便在应用中共享它们。LiveData 对象连接到系统服务一次,然后需要相应资源的任何观察者只需观察 LiveData 对象。扩展 LiveData。

LiveData和ViewModel的 官方文档

在Android的官方文档中给出了一个 饮水计数的案例

我的实现

使用LiveData和ViewModel

image.png

不使用LiveData和ViewModel

image.png

效果

rh801-todo.gif

源码

lily02

延伸

it 关键字

  1. Lambda 表达式

当使用lambda表达式时,如果lambda只有一个参数,那么可以省略参数,直接使用 it代指这个参数。

1
2
3
4
kotlin复制代码
Copy code
val list = listOf(1, 2, 3, 4, 5)
list.forEach { println(it) } // 输出: 1 2 3 4 5
  1. 作用域函数

Kotlin 提供了一些作用域函数,如 let、run、apply、also等, 它们使用it代指作为其闭包的调用对象。

1
2
kotlin复制代码val str = "Hello"
str.let { println(it.length) } // 输出: 5
  1. 解构声明

在解构声明中,it可以代表被解构的对象。

1
2
3
4
kotlin复制代码data class Person(val name: String, val age: Int)
val person = Person("Alice", 25)
val (name, age) = person // name = "Alice", age = 25
println(it) // 输出: Person(name=Alice, age=25)
  1. 其他情况

在某些特殊情况下,如类型推断无法推断出正确的类型时,可以使用it作为一个临时标识符。

1
2
3
4
kotlin复制代码val map = mapOf("a" to 1, "b" to 2, "c" to 3)
map.forEach { (key, value) ->
println("$key -> $value")
}

本文转载自: 掘金

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

ArkUI 开发实例:快五一了,做一个回家的订票动画界面(下

发表于 2024-04-25

背景

承接上篇和中篇,我们已经实现了整个页面的布局和功能逻辑,那么本章我们开始实现最后的动画效果吧。

订票动画.gif

继续实例

属性动画 (animation)

image.png

分析动效,当我们点击切换出发地和目的地按钮时,火车后面的刷新图标会进行旋转,这种组件的旋转通用属性变化时实现的动画效果,就是属性动画。

支持的属性包括width、height、backgroundColor、opacity、scale、rotate、translate等。

我们本次需要修改旋转参数,先在build()前增加一个带状态的变量

1
less复制代码@State rotateValue: number = 0; // 火车图标旋转角度

然后为当前图标增加旋转角度的属性

1
2
3
scss复制代码Image("/pages/ComponentClassification/ExampleComponents/OneReserve/reverse.png")
.width(40)
.rotate({ angle: this.rotateValue })

最后增加动画,动画支持以下的参数对象:

名称 类型 是否必填 描述
duration number 否 动画持续时间,单位为毫秒。 默认值:1000
tempo number 否 动画播放速度,值越大动画播放越快,值越小播放越慢,为0时无动画效果。 默认值:1.0
curve Curve或ICurve或string 否 动画曲线。 默认值:Curve.EaseInOut
delay number 否 动画延迟播放时间,单位为ms(毫秒),默认不延时播放。
iterations number 否 动画播放次数。默认播放一次,设置为-1时表示无限次播放。设置为0时表示无动画效果。
playMode PlayMode 否 动画播放模式,默认播放完成后重头开始播放。 默认值:PlayMode.Normal
onFinish () => void 否 动画播放完成回调。 从API version 9开始,该接口支持在ArkTS卡片中使用。
finishCallbackType FinishCallbackType 否 在动画中定义onFinish回调的类型。 默认值:FinishCallbackType.REMOVED
expectedFrameRateRange ExpectedFrameRateRange 否 设置动画的期望帧率。

根据上上述表格,动画效果默认是1000ms,默认执行一次,默认无延迟,那么我们只需要将曲线修改成EaseOut即可,此时图片代码如下:

1
2
3
4
5
6
scss复制代码Image("/pages/ComponentClassification/ExampleComponents/OneReserve/reverse.png")
.width(40)
.rotate({ angle: this.rotateValue })
.animation({
curve: Curve.EaseOut,
})

最后,回到当前按钮,设置当按钮点击后,旋转角度360度即可,按钮事件修改如下:

1
2
3
4
5
kotlin复制代码.onClick(() => {
this.ifSwitch = !this.ifSwitch;
this.ifStartSearch = false
this.rotateValue = this.rotateValue + 360;
})

按钮动画.gif

如上图所示,当前切换动画旋转效果已经实现,但是其他地方还是很生硬的,那么我们继续后续效果,接下来我们不再采用属性动画效果,而是通过转场方式实现。

组件内转场 (transition)

当用户组件出现和删除时,可以通过transition属性配置过度的动画效果,即为组件内转场动画。

其包含的参数对象如下:

接口名称 参数类型 是否静态函数 参数描述
opacity number 是 设置组件转场时的透明度效果,为插入时起点和删除时终点的值。 取值范围: [0, 1]
translate TranslateOptions 是 设置组件转场时的平移效果,为插入时起点和删除时终点的值。 -x:横向的平移距离。 -y:纵向的平移距离。 -z:竖向的平移距离。
scale ScaleOptions 是 设置组件转场时的缩放效果,为插入时起点和删除时终点的值。 -x:横向放大倍数(或缩小比例)。 -y:纵向放大倍数(或缩小比例)。 -z:当前为二维显示,该参数无效 。 - centerX、centerY指缩放中心点,centerX和centerY默认值是”50%”,即默认以组件的中心点为缩放中心点。 - 中心点为(0, 0)代表组件的左上角。
rotate RotateOptions 是 设置组件转场时的旋转效果,为插入时起点和删除时终点的值。 -x:横向的旋转向量分量。 -y:纵向的旋转向量分量。 -z:竖向的旋转向量分量。 - centerX、centerY指旋转中心点,centerX和centerY默认值是”50%”,即默认以组件的中心点为旋转中心点。 - 中心点为(0, 0)代表组件的左上角。 -centerZ指z轴锚点,即3D旋转中心点的z轴分量,centerZ默认值是0。 -perspective指视距,即视点到z=0平面的距离,perspective默认值是0。
move TransitionEdge 是 指定组件转场时从屏幕边缘滑入和滑出的效果,本质为平移效果,为插入时起点和删除时终点的值。
asymmetric appear: TransitionEffect, disappear: TransitionEffect 是 指定非对称的转场效果。 第一个参数指定出现的转场效果,第二个参数指定消失的转场效果。 如不通过asymmetric函数构造TransitionEffect,则表明该效果在组件出现和消失时均生效。
combine TransitionEffect 否 对TransitionEffect进行链式组合,以形成包含多种转场效果的TransitionEffect。
animation AnimateParam 否 指定该TransitionEffect的动画参数。 该参数只用来指定动画参数,其入参AnimateParam的onFinish回调不生效。 如果通过combine进行TransitionEffect的组合,前一TransitionEffect的动画参数也可用于后一TransitionEffect。

表格内容看似很复杂,特别是最好还有一些组合效果,但是我们先实战下简单的转场效果,实现出发地和目的地的对调,很明显这时候我们需要用到平移转场。

我们在出发地容器最后面增加转场效果,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scss复制代码Column({ space: 5 }) {
Text("信阳")
.fontSize(20)
.fontWeight(FontWeight.Medium)
.fontColor("#132968")
Text("XinYang")
.fontSize(13)
.fontColor("#132968")
}
.width(70)
.transition(
TransitionEffect.translate({
x: 200,
y: 0,
z: 0
}).animation({ duration: 1000 })
)

代表出场和入场时从x方向200的位置偏移1000ms过来。

位移出场动画.gif

很明显效果还是很生硬,我们需要加入透明度效果,实现柔和的过渡,此时需要将透明度与位移动画进行融合。

增加透明度动画后代码如下:

1
2
3
4
5
6
7
8
9
10
less复制代码.transition(
TransitionEffect.OPACITY.animation({ duration: 500, curve: Curve.LinearOutSlowIn })
.combine(
TransitionEffect.translate({
x: 200,
y: 0,
z: 0
}).animation({ duration: 1000 })
)
)

位移出场动画2.gif

很好此时动画效果很完美,那么我们在目的地也加上相应的代码即可。

目前按钮箭头还是很生硬,按钮之前我们专门切了两张图,这是因为我们需要实现垂直轴旋转的动画效果,所以我们在两张箭头图上增加沿着y轴旋转的转场180度的动画效果,当然也要加入透明度转场,此时代码如下:

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
less复制代码Button() {
if (!this.ifSwitch) {
Image("/pages/ComponentClassification/ExampleComponents/OneReserve/buttonArrow.png")
.width(40)
.transition(
TransitionEffect.OPACITY.animation({ duration: 1000, curve: Curves.springCurve(10, 1, 268, 23) })
.combine(
TransitionEffect.rotate({ y: 1, angle: 180 }).animation({ duration: 1000 }))
)
}
else {
Image("/pages/ComponentClassification/ExampleComponents/OneReserve/buttonArrowReverse.png")
.width(40)
.transition(
TransitionEffect.OPACITY.animation({ duration: 1000, curve: Curves.springCurve(10, 1, 268, 23) })
.combine(
TransitionEffect.rotate({ y: 1, angle: 180 }).animation({ duration: 1000 }))
)
}
}
.backgroundColor("#DEEBF9")
.width(40)
.onClick(() => {
this.ifSwitch = !this.ifSwitch;
this.ifStartSearch = false
this.rotateValue = this.rotateValue + 360;
})

注:此时我们用了自定义曲线,需要引入曲线模板,在当前ets顶部增加如下代码即可:

1
javascript复制代码import Curves from '@ohos.curves'

位移出场动画3.gif

最后,加下日期选择器切换到订票信息的动画效果了,

日期选择器有一个缩放的转场效果,我们在日期选择器下增加下面代码:

1
2
3
4
5
6
7
8
less复制代码.transition(
TransitionEffect.OPACITY.animation({ duration: 1000, curve: Curves.springCurve(10, 1, 268, 23) }).combine(
TransitionEffect.scale({
x: 0,
y: 0,
}).animation({ duration: 700, curve: Curves.springCurve(10, 1, 268, 23), })
)
)

此时日期选择器出现和消失时候就有缩放的动画了。

最后一步,为订票信息增加动画就好了,不过里面的信息有明显顺序,此时我们需要用到delay参数即可,为组件增加递增的delay效果。代码如下:

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
less复制代码Column() {
Row({ space: 15 }) {
Column({ space: 10 }) {
Text("日期")
.fontSize(15)
.fontWeight(FontWeight.Normal)
.fontColor("#A1A9C3")
Text((this.selectedDate.getMonth() + 1).toString() + "月" + (this.selectedDate.getDate()).toString() + "日")
.fontSize(17)
.fontColor("#132968")
}
.width(80)
.transition(
TransitionEffect.OPACITY.animation({ duration: 1000, curve: Curves.springCurve(10, 1, 268, 23) })
.combine(
TransitionEffect.scale({
x: 0,
y: 0,
}).animation({ duration: 700, curve: Curves.springCurve(10, 1, 268, 23), })
)
)

Column({ space: 10 }) {
Text("时间")
.fontSize(15)
.fontWeight(FontWeight.Normal)
.fontColor("#A1A9C3")
Text("10:08")
.fontSize(17)
.fontColor("#132968")
}
.width(80)
.transition(
TransitionEffect.OPACITY.animation({
delay: 200,
duration: 1000,
curve: Curves.springCurve(10, 1, 268, 23)
}).combine(
TransitionEffect.scale({
x: 0,
y: 0,
}).animation({ delay: 200, duration: 700, curve: Curves.springCurve(10, 1, 268, 23), })
)
)

Column({ space: 10 }) {
Text("车次")
.fontSize(15)
.fontWeight(FontWeight.Normal)
.fontColor("#A1A9C3")
Text("Z225")
.fontSize(17)
.fontColor("#132968")
}
.width(80)
.transition(
TransitionEffect.OPACITY.animation({
delay: 400,
duration: 1000,
curve: Curves.springCurve(10, 1, 268, 23)
}).combine(
TransitionEffect.scale({
x: 0,
y: 0,
}).animation({ delay: 400, duration: 700, curve: Curves.springCurve(10, 1, 268, 23), })
)
)
}

Row({ space: 15 }) {
Column({ space: 10 }) {
Text("车厢")
.fontSize(15)
.fontWeight(FontWeight.Normal)
.fontColor("#A1A9C3")
Text("07车")
.fontSize(17)
.fontColor("#132968")
}
.width(80)
.transition(
TransitionEffect.OPACITY.animation({
delay: 600,
duration: 1000,
curve: Curves.springCurve(10, 1, 268, 23)
}).combine(
TransitionEffect.scale({
x: 0,
y: 0,
}).animation({ delay: 600, duration: 700, curve: Curves.springCurve(10, 1, 268, 23), })
)
)

Column({ space: 10 }) {
Text("座位号")
.fontSize(15)
.fontWeight(FontWeight.Normal)
.fontColor("#A1A9C3")
Text("061号")
.fontSize(17)
.fontColor("#132968")
}
.width(80)
.transition(
TransitionEffect.OPACITY.animation({
delay: 800,
duration: 1000,
curve: Curves.springCurve(10, 1, 268, 23)
}).combine(
TransitionEffect.scale({
x: 0,
y: 0,
}).animation({ delay: 800, duration: 700, curve: Curves.springCurve(10, 1, 268, 23), })
)
)

Column({ space: 10 }) {
Text("票价")
.fontSize(15)
.fontWeight(FontWeight.Normal)
.fontColor("#A1A9C3")
Text("210.0元")
.fontSize(17)
.fontColor("#132968")
}
.width(80)
.transition(
TransitionEffect.OPACITY.animation({
delay: 1000,
duration: 1000,
curve: Curves.springCurve(10, 1, 268, 23)
}).combine(
TransitionEffect.scale({
x: 0,
y: 0,
}).animation({ delay: 1000, duration: 700, curve: Curves.springCurve(10, 1, 268, 23), })
)
)
}
}
.justifyContent(FlexAlign.SpaceAround)
.height(165)

搞定,当前实例完成,看下最终效果吧。

订票动画.gif

本文转载自: 掘金

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

讲一讲 2015 年著名的 XcodeGhost 风波

发表于 2024-04-25

这里每天分享一个 iOS 的新知识,快来关注我吧

前言

今天中午吃饭时撇了一眼同事电脑,发现他正在用某网盘下载 Xcode,我大惊失色,赶紧叫停,然后在吃饭的路上滔滔不绝的讲起了 10 年前发生在 iOS 开发界的那场风波。

事件起因

10 年前在国内访问苹果的服务器有时很不稳定,导致很多人下载 Xcode 比较慢,就经常有人会把下载好的 Xcode 放到网盘上方便其他人下载。

但毕竟不是通过官网途径,有一份安装包被人动了手脚,注入了一个名为 XcodeGhost 的框架,导致其编译出来的 App 会自动执行一些非开发者预期的功能。

爆发

腾讯安全团队在追踪一个 bug 时,意外发现 App 会自动收集一些信息发送到 icloud-analysis.com 这个域名,这时非预期的行为。具体收集的信息包括:app 版本、app 名称、语言、iOS 版本、设备类型、国家码等。

随后他们还发现这份恶意注入的代码不仅能够收集信息,还能够执行代码,比如唤起另一个 App、打电话、发短信等等。

为了验证感染范围,他们抽查了 App Store 上其他的 App,一些 App 中也发现了相同的问题,他们顿时感到事关重大,立即把报告上报给了国家互联网应急中心(CNCERT),CNCERT 随即发布了预警消息。

iOS 知名开发者唐巧也在微博上称 Xcode 有可能被第三方代码注入:

同一天,乌云知识库、以及国外安全公司 palo alto 也发布了相关的分析报告,XcodeGhost 事件快速升温,成为行业热点。

事后的处理

苹果显然没有在 App 的审核期间发现问题,但是事件发生后火速对感染恶意代码的 app 进行了下架处理。

被感染的 App 大都是国内应用,因此受到了下架的影响,开发人员使用苹果官方下载的 Xcode 进行打包后重新提交审核后上线。

影响范围

刚开始,腾讯的安全团队检测了 App Store 排名前 500 的应用,发现了 76 款被感染。

9月19日,360 安全播报平台检测出了 344 款感染应用。

截至2015年9月20日下午的通报,受感染 app 数量为 1078 款。

XcodeGhost 背后的黑客是谁?

随着事件的不断发酵和扩大,9月19日凌晨 4 点,一位自称是 XcodeGhost 作者发布微博道歉声明,说此次事件只是一次实验而已,并没有做过分的事情,并在 GitHub 上放出了相关源代码。大家感兴趣可以去 GitHub 上搜索一下,原仓库还在。

给我们的启示

理论上来说,任何第三方的下载网站都有风险,所以不管下载什么软件,都必须从官方下载。

这里每天分享一个 iOS 的新知识,快来关注我吧

本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!

本文转载自: 掘金

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

用 OSLog 来代替 swift 中的 print 【Ma

发表于 2024-04-25

OSLog 是苹果在 macOS 10.15(Catalina)和 iOS 15 引入的一个用于记录系统日志的新框架。它提供了一种现代化的方式来记录应用程序的日志,并且与系统日志服务集成得非常好。

在SwiftUI 应用中使用有2种常见的模式:

属性包装器(Property Wrapper)

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复制代码import os.log

@propertyWrapper
struct AppLog {
private let logger: Logger

init(subsystem: String = "com.example.MyApp", category: String = "defaultCategory") {
self.logger = Logger(subsystem: subsystem, category: category)
}

var wrappedValue: Logger {
return logger
}
}


struct MyViewModel {
@AppLog(subsystem: "com.example.MyApp", category: "MyCategory")
private var logger

func doSomething() {
// 记录日志消息
logger.log("This is a log message")
}
}

环境变量

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
swift复制代码//  applogger.swift
import Foundation
import SwiftUI
import os.log


private struct LoggerEnvironmentKey: EnvironmentKey {
static let defaultValue: Logger = Logger(subsystem: subsystem, category: "main")
}

extension EnvironmentValues {
var logger : Logger {
get { self[LoggerEnvironmentKey.self] }
set { self[LoggerEnvironmentKey.self] = newValue }
}
}

// App.swift
Settings {
SettingsView()
}
.environment(\.logger, logger)


// someView.swift

import SwiftUI
import os.log
struct ContentView: View {
@Environment(\.logger) private var logger: Logger // 访问共享日志器

var body: some View {
Text("Hello, World!")
.onAppear {
logger.info("ContentView appeared")
}
}
}

总结

两种方法都很方便,但是推荐第一种,第一种可以不用每次引入 OSLog,可以设置默认值,在需要区分的时候 再用参数区分

本文转载自: 掘金

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

微调Stable Diffusion生成你专属的毛利兰🤖️

发表于 2024-04-25

最近在研究AIGC🤖️技术,看了很多微调方法、论文层。但搞技术这么能纸上谈兵呢?今天本柯南迷就想来实践生成美少女小兰👸

在这篇文章里,我选用了 textual inversion和dreambooth📷来做比较,这两种方法都是通过扩大词字典的嵌入,来插入新的概念。

训练图片处理

image.png
在网上找到了11张高清的小兰🌸特写图,尽量找人物主体比较鲜明的图片,这样才能让模型更好地学习到人物特征~
为了图片不因为resize而变形,这里使用letterbox方法将图片先处理为512×512大小的尺寸:

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码
def letterbox_image(image, size=(512, 512), color=(0, 0, 0), scale=False):
iw, ih = image.size
w, h = size

scale = min(w / iw, h / ih)
nw = int(iw * scale)
nh = int(ih * scale)

image = image.resize((nw, nh), Image.BICUBIC)
new_image = Image.new('RGB', size, color)
new_image.paste(image, ((w - nw) // 2, (h - nh) // 2))
return new_image

到这一步,我们需要训练的人物主体数据就处理好啦~

dreambooth微调

dreambooth的效果类似于照相摄影棚——一旦拍摄了几张某个对象的照片,摄影棚就会生成包含该对象在不同条件和场景下的照片。 dreambooth的目标是扩展模型的语言视觉词典,一旦新词典嵌入模型,模型就可以在不同的背景场景下生成新词主题,同时保留其关键识别特征。

这个新词可以用标识符(identifier)来表示,为了防止语言漂移,需要在标识符的后面加入这个新词的大类,比如:“A [V] dog”,[V]为标识符,dog为大类。

image.png

为了防止语言漂移,研究者提出了 Class-specific Prior Preservation Loss

image.png

dreambooth微调代码

github.com/CrazyBoyM/d…

转换ckpt检查点文件为diffusers官方权重

这里我选用了huggingface.co/runwayml/st… 上的模型
,如果只有.ckpt文件,就需要按照readme的指示转换权重

1
2
3
4
5
js复制代码!python tools/ckpt2diffusers.py \
--checkpoint_path=./ckpt_models/v1-5-pruned_emaonly.ckpt \
--dump_path=./model \
--original_config_file=./ckpt_models/model.yaml \
--scheduler_type="ddim"

若出现加载的checkpoint和模型的不一致,需要修改statedict中的key和value。

训练object脚本的输入

下一步进入到train_object.sh脚本,我们看到在训练之前需要准备的输入:

image.png

INSTANCE_DIR: 一开始处理完的小兰的数据集的路径
CLASS_DIR: 我们还需要准备一个class的数据集,这里我把这个class设为girl,并在网上找了11张动漫少女的图片:

image.png

同样使用上面letterbox的方法对图片进行处理,最后放在CLASS_DIR下

TEST_PROMPTS_FILE:用于测试的prompt,可以使用deepdanbooru对预期的图片生成对应的标签。

最后生成的效果

prompt🌹 A photo of a doctor as Mouriran girl, in the hospital, black_hair, looking_at_viewer, smiling, open mouth, white clothes.

test-2.png

test-0 (1).png

test-0.png

prompt🌹 A photo of a professor as Mouriran girl, black_hair, looking_at_viewer, smiling, blue blouse.

professor.png

可以看出来dreambooth模型学习得比较好,仅仅1000个steps就能捕捉到小兰的特征(特别是头发那个角角),15-20分钟即可训练好。

textual inversion微调

虽然dreambooth能够较好地拟合要学习的主体的特征,但是微调了整个模型,参数量较大,而且每次学习一个新的概念都要重新训练,所以这里我们再探究一下使用textual inversion,这种更轻量级方法的效果。

比起dreambooth需要微调整个模型,textual inversion仅仅只需要微调要插入的词对应的embeddings

image.png

loss跟stable diffusion的类似:

image.png

代码仓库

这里直接使用diffusers库里examples的训练代码:
github.com/huggingface…

根据readme跑代码

修改相关参数设置

1
2
3
4
5
6
7
js复制代码--pretrained_model_name_or_path: 跟微调dreambooth用到的基础模型一样sdv1-5
--train_data_dir: 一开始处理好的小兰数据路径
--placeholder_token: <mouriran>
--initializer_token: girl
--learnable_property: object
--train_batch_size: 4
--learning_rate: 5e-3

5000个steps大概要花50分钟。。。

最后生成的效果 (5000个steps)

test-2.png

test-0.png

test-1.png

这个生成效果还是比较诡异的。。。。🤢感觉模型应该没有学好对应的embedding,仅仅是生成了漫画风的图片。

根据平时的经验来看,感觉textual inversion还是比较难训的,一是所需要的图片不止3-5张,二是很难学习到主体的细节。如果看到这篇文章的小伙伴有关于使用textual inversion的经验,欢迎评论一起讨论鸭~😃

本文转载自: 掘金

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

Solidity的开发工具介绍

发表于 2024-04-25

古人云:工欲善其事,必先利其器。开发Solidity也是,选择一个合适的开发环境是必要前提。下面来介绍一下常用的开发工具。

remix

首先,如果是初学者,remix 是一个很方便的工具。只需要打开网页,就可以开始编写第一行代码,适合新手,可以在浏览器中快速部署测试智能合约,不需要在本地安装任何程序。

进入remix,我们可以看到最左边的菜单有三个按钮,分别对应文件(写代码的地方),编译(跑代码),部署(部署到链上)。我们点新建(Create New File)按钮,就可以创建一个空白的solidity合约。

文件tab

1-1-59ec4df354181363259759212e42dad1.png

编译tab

企业微信截图_fb8081ae-4b6a-47ba-90cc-e605ba6f3698.png

编译版本的选择要注意,不同的编译版本,可能对应的函数不同

部署 tab

image.png

在正常情况下,remix会用JS虚拟机来模拟以太坊链,运行智能合约,(里面也有其他地方的虚拟机可以选择),类似在浏览器里跑一条测试链。并且remix会分配几个测试账户给你,每个里面有100 ETH(测试代币),可劲儿用。你点Deploy(黄色按钮),就可以部署咱们写好的合约了。

部署完成后

image.png

你会在下面看到名为HelloWeb3的合约,点击_string,就能看到我们代码中写的 “Hello Web3!” 了。


VSCode

VSCode 也是开发 solidity 的工具之一,不过需要安装一些插件和配置环境。

下面将列一下需要的环境。

  1. nvm:npm 的包管理器(已有可以忽略)
  2. npm:是随同Node.js一起安装的包管理工具(必须安装)
  3. yarn:和npm差不多 都是安装依赖包的(可选)
  4. truffle:solidity 的工具,可以编译、链接、部署和二进制管理。(可选,也可以通过VSCode 安装 Truffle for VS Code)
  5. Ganache:可视化应用程序开发的个人区块链,后期可用。(可选)

具体的安装方式可以在文章结尾找到

环境安装好后,就可以打开VSCode,安装插件了

  1. 扩展里搜索solidity,安装即可

image.png

2.扩展里搜索 truffle 安装即可

image.png

这两个插件安装成功后,基本上就可以开始敲代码了。

可以用Truffle for VS Code 的脚手架创建新的 solidity 空项目

第一步 选择命令
image.png

第二步 创建新 solidity 项目
image.png

第三步 选择空项目

image.png

第四步 选择一个文件夹,将新建的项目放进去

第五步 创建完成

image.png

解释

1
2
3
4
5
6
7
less复制代码--build: 存放编译后文件的目录(编译后生成)
--contracts: solidity合约代码的目录
--migrations: 用 js 语言写的部署用的文件
--node_modules: js的项目依赖(执行 npm init后生成)
--test: 用 js 语言写的单元测试目录
--package.json: 依赖的配置文件(npm 配置管理)
--truffle-config.js: truffle框架的配置文件

大多数情况这个项目需要引入 npm 包
所以打开 vscode 的终端 在当前文件夹下,输入npm init
因为刚创建的空文件是没有 “package.json” 这个文件的,所以需要创建一个,输入npm init后,按照要求会生成一个“package.json”文件

image.png

这时就可以看到 文件夹内有了 “package.json”文件

image.png

然后,就可以通过 npm 安装别的依赖包了,比如 openzeppelin(Solidity开发必用的库)

执行npm install @openzeppelin/contracts

安装完成后,就可以看到文件结构

image.png

至此,必要的安装工具就已经介绍完毕,所以如果要在 vscode 上开发,还是需要安装很多东西。

走完第一步,后面的路就好走了,加油!


nvm、npm 安装可以参考这个

yarn 安装可以参考这个

truffle 安装可以参考这个

Ganache 安装可以参考这个

本文转载自: 掘金

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

智能体开发现状与思考

发表于 2024-04-25

借着AI大模型的风,各家大模型陆续商用,基本都提供api调用或是提供平台免费开发和试玩。我开发了《逃出生天系列》游戏,同时把智能体发布到智谱清言、扣子(掘金)、冒泡鸭、也构建了独立的web端,总用户800+。各个平台不仅是模型端还有平台端都处在快速迭代疯狂获取流量阶段。短时间内,蓝海变红海。也亲身感受了这波热潮。沉淀下感受和思考。一家之言,很多内容是推测,不喜勿喷。

蓝海变红海原因

开发低门槛&免费试用

仔细分析下内在原因,首先是门槛低到能打字就能开发。AI能力免费的吸引力,风口来了总要看一看。有些人想要感受下AI的能力,也有有些人想深入挖掘AI能力和基于能力搭建应用。所以大量开发者和使用者涌入平台,智能体疯狂涌现。做一个智能体来进行文本游戏就是我在干的事情。

智能体壁垒低

一个智能体的差异主要有两点:1. idea 抄起来不能更简单了 2. prompt编辑能力。但是prompt的差异比较隐式。需要深入使用才能逐渐清晰,再加上大模型的不确定性导致测试和评价难度增大。另外你还可以通过特定的输入套出别的智能体的prompt,从而快速抄袭。结果就是同类相似的智能体涌现后让数量更加爆炸。创意哪有那么多,自己的智能体也很难守得住。所以,智能体的多样性就很快遇到瓶颈,很快就到了行业细分和垂直领域挖掘需求的程度了。

智能体筛选乏力

由于智能体数量增长迅速,现在平台也只能通过简单的访问量、使用量和收藏等维度进行衡量。在受到标题党,美图党,先发优势等原因的影响,这些衡量因素更不能真正反馈智能体的好坏。所以很多智能体很容易长期霸榜,优质智能体淹死在红海中。

下面就是我的两个智能体的数据,左边是精致迭代15版4000字的prompt,右面就随便写写的标题党。
image.png

所有无论是智能体开发者和用户都在疯狂后,进入到冷静贤者阶段。

开发者热情下降

优质智能体无法在红海崭露头角

在庞大的数量面前,新智能体的自然流量锐减。另外智能体的体验也需要花费一些精力。而且用户需要深度使用后才会有更加中肯的评价。最终用户点了个收藏。如此转化率下,新智能体很难出头。当然有些平台有推荐,例如扣子有推荐机制,但是推荐页内智能体数量很多,质量嘛也不好多说。

智能体迭代困难,无用户反馈

智能体也是需要版本迭代,不断调优。这和代码开发类似。但是测试过程却很不相同。代码开发自测基本就够了,由于智能体的不确定性和自己思维的局限性导致智能体需要批量测试。同时也更加依赖用户的使用细节。

在各个平台发布的智能体,除了掘金的沸点中可以看到用户的游玩记录外,其他的均无法拿到用户的输入和反馈。只能看到简单的用户数和流量等概要数据。用户是怎么玩的作为开发者无从得知。
可能是用户按照开发者的设计正常使用,也可能用户在越狱后疯狂自嗨。也可能用户被标题吸引,用户玩了两下就退出了。开发者不得而知,更无法形成用户反馈到优化产品的闭环。

当然,向开发者隐藏用户数据可能是出于平台设计或者用户隐私等各种原因而制定的规则,但这确实影响了智能体后续发展。大量低质量的智能体和不迭代的智能体就像无人维护的代码一样,很容易变成负债。除非这个智能体已经好到不用优化了。

这也是为什么我需要自己搭建web端的原因。我可以收集匿名用户使用的信息,从而针对性的优化智能体。

优质开发者无收益

智能体开发者分为两种:一般开发者:随便玩玩看,发了智能体之后就没有之后了。优质开发者:例如我,有一定的代码开发能力,希望开发优质的智能体,基于prompt深度挖掘大模型的能力和探索能力的应用场景。

平台与智能体是能力与应用的关系。相互依靠,相互促进。平台能力越强,越应该孵化出更优质的智能体。优质的智能体也能更具象的展示平台的能力和价值。试想一下,如果平台能力出众,但用户都用来玩各种角色扮演和羞羞体验,是不是也挺郁闷的。

我将自己精细开发的prompt发布到各个平台后,也就没有然后了。我希望将流量引回我的web端,也不是所有平台允许的。

即使平台提供相关激励活动,但由于筛选智能体能力的匮乏,也无法有效惠及优质开发者。

新鲜感后,用户热情下降

相似的智能体给用户带来困扰。

智能体数量带来选择的困难。

智能体质量参差,带来不稳定的体验。

智能体的约束能力不足,越狱泛滥,而且用户精力飞散,更不愿深入的体验智能体。优质智能体在用户端也很难崭露头角。

思考&建议

建立符合智能体特点筛选能力

结合prompt质量和用户使用过程的筛选方式,而不是唯流量论。完善智能体的评价体系,例如可以从新奇度,严谨度,趣味度,价值度等维度进行综合评价。也可以考虑用ai评价ai,完成初筛。后续可以人工试玩筛选,虽然人力成本较高,但也是最准确的。

建立和优化推荐机制

推荐必是官方推荐的精品。

优化推荐机制让流量的分配更合理。避免出现流量大户和过多流量进入低质量智能体。

维护支持优质智能体开发者

在告知用户的前提下,为开发者提供更多用户使用智能体的信息。

具体操作就更多了,签约,招募,流量支持,活动奖励,允许引流、官方认证、官方推荐等等。

本文转载自: 掘金

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

Playwright Python 自动化测试:零基础半个月

发表于 2024-04-25

背景:前端开发,公司项目要写ui自动化,没测试经验,过了一遍Python语法,然后开始了自动化测试的学习,这里记录一下使用中用的到的知识点

引言

在较长的产品周期中,自动化测试是确保应用质量和性能的关键步骤。这里记录了一个前端开发者从零开始,学习Python和Playwright来构建自动化测试的过程。Playwright 是一个强大的自动化测试工具,支持多种浏览器,本文将从基础安装到脚本编写的每一个步骤进行详细介绍,重点是示例。

建议:等你遇到问题再来查,或许会帮助你解决

测试大佬勿喷,欢迎指正

playWright 自带的元素获取工具

效果展示:

20240423-175716.gif

工具源文档

playwright codegen 是 Playwright 测试框架的一个功能,它可以自动记录您在浏览器中的操作并生成相应的测试代码。这是一个非常有用的工具,尤其是对于快速开始编写测试或者为现有的用户操作创建自动化测试脚本。

使用 playwright codegen 的步骤如下:

安装 Playwright:如果您还没有安装 Playwright,可以通过下面命令安装

1
复制代码npm install playwright

运行 Codegen:在命令行中输入 npx playwright codegen 后跟您想要测试的网站的 URL。例如:

1
arduino复制代码npx playwright codegen https://www.baidu.com

如果您不提供 URL,codegen 会打开一个空白的浏览器窗口,您可以在其中输入您想要测试的网站的 URL。

记录操作: 在打开的浏览器窗口中执行您想要测试的操作。Playwright 会监视您的操作并生成相应的测试代码。

生成断言: 您可以通过点击 Playwright Inspector 窗口中的图标来生成断言,然后点击页面上的元素来对其进行断言。

停止记录: 完成操作后,点击“停止记录”按钮。然后,您可以使用“复制”按钮将生成的代码复制到您的编辑器中。

编辑和保存测试: 您可以在编辑器中进一步编辑和完善生成的测试代码,然后将其保存为测试文件。 playwright codegen 是快速生成测试代码的强大工具,它可以节省时间并确保测试准确录制了用户的实际操作。

元素获取示例

语法page.locator()

page.locator(".menu-delete-modal") 是 Playwright 测试库中的一个语法,用于定位页面上的元素,并且可以链式调用多种操作。这里,.menu-delete-modal 是一个CSS类,它会选择页面上所有具有 menu-delete-modal 类的元素。page.locator 方法返回一个 Locator 对象,代表了页面上的一个或多个DOM元素,您可以对这些元素执行各种操作,比如点击、填写表单等。

以下是一些 Python 示例,展示了获取元素的常用方法page.locator():

  1. 定位元素:使用 CSS 选择器或 XPath 来定位页面上的元素。
1
bash复制代码page.locator('.example').click()  # 点击类名为example的元素
  1. 文本内容定位:通过元素的文本内容来定位。
1
bash复制代码page.locator('text="登录"').click()  # 点击文本为“登录”的元素
  1. 属性定位:通过元素的属性来定位。
1
bash复制代码page.locator('[name="email"]').fill('example@example.com')  # 填写name属性为email的输入框
  1. 等待元素:等待元素出现在页面上。
1
ini复制代码page.locator('.loading').wait_for(state="hidden")  # 等待加载元素消失,默认等待元素出现
  1. 获取元素属性:获取元素的属性值。
1
ini复制代码href = page.locator('.link').get_attribute('href')  # 获取链接的href属性
  1. 处理多个元素:对页面上的一组元素进行操作。
1
2
3
ini复制代码items = page.locator('.list-item').element_handles()
for item in items:
   item.click()  # 点击每个列表项
  1. 元素的父级元素: 拿到父级,获取父级下其他数据是。
1
ini复制代码parent_element = page.locator('text="登录"').locator('xpath=..')

示例-> 获取类名 menu-delete-modal 弹框下一个按钮

1
arduino复制代码page.locator(".menu-delete-modal").get_by_role("button", name="确定")

示例-> 使用locator选择器来点击一个确定按钮

1
scss复制代码page.locator(".menu-delete-modal").click()

注意:在严格模式,返回的是一个或多个元素,存在差异。如果是一个元素,可以直接执行Locator对象上的元素操作方法,一些静态方法,如:click(), inner_html()…;多个元素的情况不能执行click(),inner_html()…;通过count() 可以查看有多少元素。如图

一个元素:

image-20240409103518052.png

多个元素:

image-20240409103622830.png

想要点击其中一个元素可以使用.nth(index),其中 index 是从 0 开始的索引:

1
2
3
scss复制代码page.locator(".el-button").nth(0).click()  # 点击第一个按钮
page.locator(".el-button").nth(1).click() # 点击第二个按钮
# 以此类推...

如果需要对所有匹配的元素执行操作,可以遍历它们:

1
2
3
4
5
6
7
8
9
10
11
ini复制代码# 方式1
buttons = page.locator(".el-button")
count = buttons.count()
for i in range(count):
buttons.nth(i).click()

# 方式2
button_handles = page.locator(".el-button").element_handles()
# 遍历并点击每个按钮
for button_handle in button_handles:
button_handle.click()
  1. page.get_by_role()

get_by_role 是 Playwright 测试库中的一个功能,它允许您根据元素的 ARIA 角色 来定位页面上的元素。当您使用 get_by_role 时,通常也会传递一个可访问名称,以便精确地定位到特定元素。

例如,如果您想定位并且点击页面上名为“登录”的按钮,可以使用以下代码:

1
arduino复制代码page.locator(".menu-delete-modal").get_by_role("button", name="确定").click()

这里按钮在浏览器的元素界面是button下包裹 span,确定文字在span内,如下:

image-20240409102104163.png

name的值采用的是模糊匹配,name为:确,也可以正常执行

对前端开发者来说,直接使用 HTML 元素和类名选择器更直观和方便。ARIA 角色主要是为了提高网页的无障碍性,帮助使用辅助技术的用户更好地理解和导航网页内容。

示例-> get_by_text 精准获取一个元素

界面:
image-20240411173707556.png

代码如下:

1
arduino复制代码page.locator('.menu-config-dropdown-menu').get_by_text('员工')

执行发现会获取两个元素

{Error}strict mode violation: locator(“.menu-config-dropdown-menu”).get_by_text(“员工”) resolved to 2 elements:

  1. …
    aka get_by_text(“员工”, exact=True)

  2. …
    aka get_by_text(“全部员工”)

想要精准匹配,如下哦

1
ini复制代码page.locator('.menu-config-dropdown-menu').get_by_text('员工', exact=True)

示例-> 获取元素的父级元素

期望的效果是通过标题名称获取到包装元素,最终调用如下:

1
scss复制代码menu_form_design_page.get_component_by_comp_name('单行输入333').click()

场景如下:

image-20240419145205549.png

函数如下:

1
2
3
4
5
6
7
8
9
10
python复制代码def get_component_by_comp_name(self, comp_name: str) -> ComponentWrapper:
"""
# 通过组件名获取组件,返回一个组件
"""
component = self.form_item_list.get_by_text(f'{comp_name}', exact=True).first # 通过名称匹配到标题
if component.count() != 1:
raise Exception(f'未找到组件{comp_name}')
while component.get_attribute('data-ui-test-component-type') is None: # 循环向上找目标父元素
component = component.locator('xpath=..')
return ComponentWrapper(self.page, component) # 自定的一些目标元素包装类,用于封装一些常用属性和方法

ComponentWrapper类如下:

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
python复制代码from playwright.async_api import Locator, Page

class ComponentWrapper:
def __init__(self, page: Page, component_locator: Locator):
self.page = component_locator.page
self.comp_locator = component_locator
self.comp_type = component_locator.get_attribute("data-ui-test-component-type")
self.comp_name = component_locator.get_attribute("data-ui-test-component-label")
self.copy_btn = component_locator.locator(".svg-x-lib-copy-add").first
self.delete_btn = component_locator.locator(".svg-delete-icon").first
self.delete_dialog = page.locator(".delete-component-modal").first
self.cancel_btn = self.delete_dialog.get_by_role("button", name="取消")
self.confirm_btn = self.delete_dialog.get_by_role("button", name="确认")
self.comp_locator.click = self.component_wrapper_click # 重写click方法

# 删除组件
def delete_comp(self):
self.comp_locator.click()
self.delete_btn.click()
if self.delete_dialog.is_visible():
self.confirm_btn.click()

# 点击方法
def component_wrapper_click(self):
self.comp_locator.evaluate("element => {element.click()}") # !!可以执行JavaScript代码,前端觉得很合理🦫

元素操作

示例-> 拖拽元素

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
ini复制代码# 向指定子表组件中添加指定类型组件,传入指定子表名称和组件类型列表
def add_component_to_specified_son_table(self, son_table_name: str, component_enum_list: list[OriginComp], callback_fn: callable = None):
# 根据名称获目标表格组件
son_table = self.get_component_by_comp_name(son_table_name)
# 悬停在目标配置区域,滚动鼠标到子表组件的位置,之前发现的一个问题,如果子表格组件不在可视区域内,会导致无法拖拽某些组件会有问题
self.form_item_list.hover()
self.page.mouse.wheel(
delta_y=son_table.comp_locator.evaluate("(el) => el.getBoundingClientRect()")['y'],
delta_x=0
)
# 断言子表格组件的数量为1,如果不为1,说明定位到的不是唯一的子表格组件
assert son_table.comp_locator.count() == 1
# 获取原始组件列表,被拖拽内容
component_type_list = Components.comp_enum_list_to_type_list(component_enum_list)
component_list = self.get_origin_components(component_type_list)
for component in component_list:
component.hover()
self.page.wait_for_timeout(500)
self.page.mouse.down()
# 获取子表格组件的矩形区域
form_panel_form_widget_son_table_rect = son_table.comp_locator.locator(
'> .form-widget-son-table .grid-draggable > .drag-area'
).evaluate("(el) => el.getBoundingClientRect()")
# 计算矩形区域的x和y坐标,相对于页面左上角
form_panel_form_widget_son_table_rect_x = form_panel_form_widget_son_table_rect['x'] + 50
form_panel_form_widget_son_table_rect_y = form_panel_form_widget_son_table_rect['y'] + 80

# 移动鼠标到计算出的y坐标位置,分两步也是因为有些组件从左上角开始拖拽会有问题,所以先向下,再向右
self.page.mouse.move(
x=0,
y=form_panel_form_widget_son_table_rect_y,
steps=10
)
self.page.mouse.move(
x=form_panel_form_widget_son_table_rect_x,
y=form_panel_form_widget_son_table_rect_y,
steps=10
)
self.page.wait_for_timeout(500)
self.page.mouse.up()
# 完成一次拖拽操作,之后执行回调函数。我在Python里面写JavaScript代码,前端觉得很合理🦫
if callback_fn:
callback_fn()

示例-> Python playWright 执行浏览器JavaScript脚本代码

  1. 点击元素
1
2
3
4
5
python复制代码def click_element(self, element: Locator):
"""
# 点击元素
"""
element.evaluate("element => {element.click()}")
  1. 有的时候playWright的click()方法会有问题,点击不到目标元素,可以使用JavaScript代码来执行点击操作

下面是一个组件的封装类,里面覆盖了点击元素的方法,使用了evaluate方法执行JavaScript代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码from playwright.async_api import Locator, Page
class ElementManager:
def __init__(self, page: Page, element_locator: Locator):
self.page = element_locator.page
self.elem_locator = element_locator
self.elem_type = element_locator.get_attribute("data-test-elem-type")
self.elem_label = element_locator.get_attribute("data-test-elem-name")
self.copy_element = element_locator.locator(".icon-copy-element").first
self.remove_element = element_locator.locator(".icon-remove").first
# 重写点击事件
self.elem_locator.click = self.element_click_action

# 元素点击动作
def element_click_action(self):
self.elem_locator.evaluate("element => {element.click()}")

附件上传

1
2
3
4
5
6
7
8
9
10
python复制代码class AddImportTemplateUploadInput:
__init__(self, page: Page):
self.page = page
self.add_import_template_upload_input = self.page.locator(
".el-upload > .el-upload__input").first

# 上传execl模版,参数为文件路径
def upload_excel_template(self, file_path):
self.add_import_template_upload_input.set_input_files(file_path) // 找到input元素,然后设置文件路径
self.page.wait_for_timeout(1000)

元素断言

常用语法

以下是一些Playwright测试框架中常用的断言方法:

  1. to_be_visible():验证元素是否可见。这是最常用的断言之一,因为它可以快速检查元素的显示状态。
1
2
scss复制代码# 验证元素是否隐藏
expect(page.locator('#element')).to_be_visible()
  1. to_have_text(text):验证元素是否包含特定的文本内容。这个断言也很常用,因为它可以确认元素中的文本是否符合预期。
1
2
bash复制代码# 验证元素是否包含特定的文本内容
expect(page.locator('#element')).to_have_text('Welcome, user!')
  1. to_be_editable():验证元素是否可编辑。这个断言对于表单元素来说非常重要,确保用户可以输入数据。
1
2
scss复制代码# 验证元素是否可编辑
expect(page.locator('#input')).to_be_editable()
  1. to_have_class(class_name):验证元素是否具有特定的类名。类名通常与元素的视觉和功能特性相关联。
1
2
bash复制代码# 验证元素是否具有特定的类名
expect(page.locator('#element')).to_have_class('active')
  1. to_have_attribute(name, value):验证元素是否具有特定的属性和值。属性值可以提供关于元素状态的重要信息。
1
2
csharp复制代码# 验证元素是否具有特定的属性和值
await expect(page.locator('#element')).to_have_attribute('class', '')
  1. to_have_css(property, value):验证元素是否具有特定的CSS属性和值。CSS属性影响元素的布局和外观。
1
2
bash复制代码# 验证元素是否具有特定的CSS属性和值
expect(page.locator('#element')).to_have_css('display', 'none')
  1. to_have_js_property(name, value):验证元素是否具有特定的JavaScript属性和值。这些属性可能会影响元素的行为和状态。
1
2
python复制代码# 验证元素是否具有特定的JavaScript属性和值
expect(page.locator('#checkbox')).to_have_js_property('checked', True)
  1. to_have_id(id):验证元素是否具有特定的ID。ID是元素的唯一标识符,通常用于精确定位。
1
2
bash复制代码# 验证元素是否具有特定的ID
expect(page.locator('#element')).to_have_id('unique-id')
  1. to_have_count(count):验证页面上特定元素的数量是否符合预期。这个断言用于检查元素集合的大小。
1
2
scss复制代码# 验证页面上特定元素的数量是否符合预期
expect(page.locator('.item')).to_have_count(4)
  1. to_be_focused():验证元素是否获得焦点。焦点状态对于交互元素来说非常重要。
1
2
scss复制代码# 验证元素是否获得焦点
expect(page.locator('#input')).to_be_focused()
  1. to_be_hidden():验证元素是否隐藏。这与to_be_visible()相对,用于检查元素是否未显示。
1
2
scss复制代码# 验证元素是否隐藏
expect(page.locator('#element')).to_be_hidden()
  1. to_be_empty():验证容器元素是否为空。这个断言用于确认容器类元素中没有子元素。
1
2
scss复制代码# 验证容器元素是否为空
expect(page.locator('#container')).to_be_empty()

判断 是否有 某个元素

1
2
3
4
5
6
python复制代码async def element_exists(page, selector):
return await page.locator(selector).count() > 0

# 使用示例
exists = await element_exists(page, '#element-id')
print('元素存在:', exists)

工具方法

示例-> Python中的指定文件夹压缩,类似于鼠标右键 -> 压缩该文件夹📂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
lua复制代码def compress_directory(file_path, zip_file_path):
# 判断file_path是否存在
if not os.path.exists(file_path):
raise Exception(f'Error: {file_path} not found')
if os.path.exists(zip_file_path):
os.remove(zip_file_path)

with zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
base_path = os.path.abspath(file_path)
for root, dirs, files in os.walk(file_path):
for file in files:
if file.startswith('.~'): // 跳过某些文件
continue
if file.startswith('~'): // 跳过某些文件
continue
file_path = os.path.join(root, file)
relative_path = os.path.relpath(file_path, base_path)
zipf.write(file_path, arcname=relative_path)

使用示例:

1
2
3
ini复制代码file_path = '/Users/xxx/Documents/tests/app_test_assets/xxx系统场景测试模版'
zip_file_path = '/Users/xxx/Documents/tests/app_test_assets/zip/xxx系统场景测试模版.zip'
compress_directory(file_path, zip_file_path)

避坑📢:mac解压会自动包裹一层文件夹

小白经验,欢迎指正

本文转载自: 掘金

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

在Swift中使用Actors防止数据竞争 在Swift中使

发表于 2024-04-25

在Swift中使用Actors防止数据竞争

hudson 译 原文

数据竞争——所有开发人员最糟糕的噩梦!它们很难检测,非常不可预测,而且极难修复。苹果为开发人员提供了各种工具集,如NSLock和串行队列,以防止在运行时发生数据竞争,然而,它们都无法在编译时捕获竞争条件。随着Swift 5.5的发布,情况将不再如此!

Swift 5.5引入了Actor,这是新的Swift语言功能,可以帮助开发人员在开发期间捕捉任何可能的竞争条件。在本文中,我们将首先了解在使用调度队列(DispatchQueue)和异步任务时如何发生数据竞争。然后,我们将看看Actors如何帮助我们识别代码中的竞争条件,并一劳永逸地防止它们发生!

不要再浪费时间了,让我们直接开始吧。

数据竞争是如何发生的?

当2个或更多线程试图同时异步访问(读/写)相同的内存位置时,就会发生数据竞争。在Swift的上下文中,当我们尝试使用调度队列修改对象的状态时,通常会发生这种情况。我这是什么意思?

考虑以下Counter类,该类具有count计数变量,每次调用addCount()函数时,它都会增加1:

1
2
3
4
5
6
7
8
swift复制代码class Counter {

var count = 0

func addCount() {
count += 1
}
}

现在,假设我们有一个按钮,可以触发以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
swift复制代码let totalCount = 1000
let counter = Counter()
let group = DispatchGroup()

// Call `addCount()` asynchronously 1000 times
for _ in 0..<totalCount {

DispatchQueue.global().async(group: group) {
counter.addCount()
}
}

group.notify(queue: .main) {
// Dispatch group completed execution
// Show `count` value on label
self.statusLabel.text = “\(counter.count)”
}

基本上,上述代码的作用是使用调度组异步调用Counter的addCount()函数1000次。一旦调度组执行完成,我们将在一个标签控件上显示Counter的count的值。

理想情况下,每次点击按钮时,我们都应该看到标签上显示1000,但情况并非如此。我们得到的结果非常不一致,我们可能会偶尔得到1000,但我们得到的值通常小于1000。
在这里插入图片描述

正如您可能已经猜到的,这种不一致是由数据竞争引起的。当多个线程(由调度队列生成)尝试异步访问计数时,不能保证每个线程会一个接一个地更新计数值。因此,导致我们获得的最终结果非常不一致,很难预测。

专业提示:

Xcode有一个Thread Sanitizer,可帮助开发人员以更一致的方式检测数据竞争。您可以通过导航到Product > Scheme > Edit Scheme…
来启用它。 之后,在编辑方案对话框中选择 运行>诊断>,勾选Thread Sanitizer复选框。

在这里插入图片描述

现在您已经知道数据竞争是如何发生的,如果我们使用异步/等待和异步任务做同样的事情,会发生数据竞争吗?让我们来了解一下!

异步任务和数据竞争

在Swift并发领域,任务和任务组的工作原理与调度队列和调度组类似。我们可以通过创建一个父任务来实现之前的数据竞争条件,该任务生成一组异步执行addCount()函数的子任务。方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
swift复制代码let totalCount = 1000
let counter = Counter()

// Create a parent task
Task {

// Create a task group
await withTaskGroup(of: Void.self, body: { taskGroup in

for _ in 0..<totalCount {
// Create child task
taskGroup.addTask {
counter.addCount()
}
}
})

statusLabel.text = “\(counter.count)”
}

在上面的代码中,我们使用withTaskGroup(of:body:)方法来创建任务组。在任务组中,我们创建1000个子任务,以异步执行addCount()函数。值得一提的是,withTaskGroup(of:body:)方法是可等待的,因此它将暂停,直到所有子任务完成。一旦发生这种情况,我们将在标签上显示计数值。

当我尝试运行上面的代码时,我得到的结果出乎意料地一致!每次代码完成执行时,我都可以看到标签上显示1000。这是否意味着当我们使用任务和任务组时,数据竞争不会发生?🤔

不幸的是,答案是否定的!

当我尝试在启用Thread sanitizer 的情况下运行上述代码时,我仍然会收到Thread sanitizer 警告,这表明确实发生了数据竞争。

在这里插入图片描述

如果是这样,那么我们为什么能够获得如此一致的结果呢?我的猜测是,苹果在优化整个Swift并发模块方面做得很好,因此它能够解决简单的数据竞争条件,就像我们在示例代码中一样。

使用调度队列时,我们可以通过使用串行调度队列来防止并发写入来避免数据竞争。如果我们使用异步任务,我们应该使用什么来防止并发写入?这就是引进Actor的原因。

充当救援队的Actor

Actor是Swift 5.5中引入的一项新语言功能,主要用于帮助开发人员在开发期间识别任何可能的数据竞争条件。正如您稍后将看到的,每当我们尝试编写可能导致数据竞争的代码时,编译器都会给我们一个编译错误。如果你不熟悉Actor的工作方式,你可以参考我之前的文章,其中谈到了Actor的基础知识。

现在让我们试着把Counter类换成一个actor。我们需要做的是用actor取代class ,仅此而已!

1
2
3
4
5
6
7
8
swift复制代码actor Counter {

private(set) var count = 0

func addCount() {
count += 1
}
}

在这个阶段,我们的示例代码将在我们尝试访问count变量的地方给出2个编译错误。

在这里插入图片描述

错误消息“Expression is ‘async’ but is not marked with ‘await’”到底是什么意思?这意味着我们不能像这样简单地访问count变量!

由于Counter现在是一个actor ,它一次只允许1个异步任务访问其可变状态(count变量)。因此,如果我们要访问count变量,我们必须将两个访问点标记为await,指示如果有另一个任务访问count变量,这些访问点可能会暂停。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
swift复制代码let totalCount = 1000
let counter = Counter()

// Create a parent task
Task {

// Create a task group
await withTaskGroup(of: Void.self, body: { taskGroup in

for _ in 0..<totalCount {
// Create child task
taskGroup.addTask {
// Marked with await
await counter.addCount()
}
}
})

// Marked with await
statusLabel.text = “\(await counter.count)”
}

值得一提的是,actors将保护其可变状态免受[多线索]读写访问。这就是为什么我们的示例代码中的两个访问点上都出现了编译错误。

情况就是这样,我们介绍了如何通过使用Actor来防止数据竞争赛。如果你想亲自尝试示例代码,请在这里 随时获取。

小结

在Swift中引入Actors绝对是值得欢迎的。它使我们能够用很少的编码工作来编写更安全的异步代码。事实上,它是一个语言功能,使它能够在编译时捕获任何可能的竞争条件,从而防止我们意外地将数据竞争引起的错误发送给我们心爱的应用程序用户。

感谢您的阅读。👨🏻‍💻

本文转载自: 掘金

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

1…181920…956

开发者博客

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