CppCon 2018 学习:THE MOST VALUABLE VALUES

这段文本进一步探讨了 值语义编程语言 中的相关概念,尤其是在 HaskellC 编程中如何使用语言来交换和操作"值"。我将逐一解释其中的每个部分。

值语义 (Value Semantics)

值语义 强调了值本身的独立性。在编程中,值是可以传递的,并且修改这些值不会影响其他地方的值。值通常是不可变的。在这部分中提到的 42blue"Juanpe Bolívar"f(x) = x⁴(自然数集)都是典型的值,它们在不同上下文中是独立的,不会因为其他值的变化而改变。

Haskell 示例

接下来,给出了一个 Haskell 中的例子:

haskell 复制代码
let x = 42 
    y = x + 2 
    z = [ 1, 2, 3, 4 ] 
    f = λ x -> x ^ 4 
    w = map f z
  • let x = 42 定义了一个值 x,其值为 42
  • y = x + 2 通过 x 来定义 y ,这表示 yx 加 2 的结果。由于 值不可变y 是一个独立的值,它的创建和 x 的值没有直接关系。
  • z = [1, 2, 3, 4] 定义了一个列表 z
  • f = λ x -> x ^ 4 定义了一个函数 f,它接受一个参数 x,并返回 x 的四次方。
  • w = map f z 使用 map 函数将 f 应用到列表 z 中的每个元素,生成一个新列表 w
    这段 Haskell 代码展示了值语义的特点:值的不可变性和通过函数式编程进行操作。

C 示例

紧接着,给出了一个 C 编程的例子:

c 复制代码
int foo = 42; 
int* p = &foo; 
*p = foo + 1;
  • int foo = 42; 定义了一个整数变量 foo,并将其值初始化为 42
  • int* p = &foo; 定义了一个指针 p,它指向 foo 的内存位置。
  • *p = foo + 1; 通过指针 p 修改了 foo 的值,将其设置为 foo + 1(即 43 )。
    与 Haskell 中的值语义不同,C 使用 引用(指针) 来直接修改内存中的值,这种方式更接近 对象语义,即对象的值可以直接修改和改变。

Haskell 与 C 的对比:

  • Haskell 中的 值语义 强调的是不可变的值,每个值是独立的,并且不会随着其他值的改变而变化。你传递的是值的副本,不会修改原始值。
  • C 中的 引用语义(通过指针操作)允许你直接操作内存中的地址,改变原始值。

对象、位置、类型、生命周期

  • 对象 (Object): 在编程中,对象 通常指代一种具有 状态行为 的实体,特别是在面向对象编程中。对象可以在程序的不同地方传递,并在不同的上下文中表现出不同的行为。
  • 位置 (Location): 这指的是值或对象在内存中的存储位置。在 C 中,指针变量存储的是对象在内存中的 地址,因此可以通过该地址修改对象的值。
  • 类型 (Type): 每个值或对象都有一个类型,它决定了该值或对象的合法操作及其存储方式。在 C 中,int foo 的类型是 整数 ,在 Haskell 中,x = 42 的类型是 整数 类型。
  • 生命周期 (Life-time): 这是指对象或变量在程序中的有效期。例如,在 C 中,局部变量的生命周期通常由其作用域决定,而在 Haskell 中,值的生命周期通常由垃圾回收机制管理。

值、指针与内存地址:

在 C 中,指针操作是值的传递的核心机制。通过指针,我们不仅传递数据的 副本 ,还可以直接操作数据在内存中的 位置 ,这与 值语义 在 Haskell 中的概念是不同的。通过指针,C 程序能够更灵活地修改数据,甚至能够改变数据的 类型生命周期

总结:

  • 值语义 (Value Semantics) 强调的是数据的独立性和不可变性,尤其在 Haskell 中非常典型。每次操作都会产生一个新的值,而不修改原始数据。
  • C 语言中的指针和引用 允许对数据进行修改,强调 内存地址和位置,这使得数据的修改直接影响原始数据。
  • Haskell 中,数据的类型和生命周期更容易受到控制,因为它遵循不可变性原则;而在 C 中,程序员需要明确地管理内存,并处理类型和生命周期的变化。
    通过对比这两种编程方式,可以更好地理解 值语义引用语义 (或称为 对象语义)之间的差异。

涉及了很多编程概念,特别是 面向对象编程函数式编程 中的术语。它还探讨了 抽象值语义引用语义可变状态 等概念,并提到了几种不同的设计范式和工具。我们来逐个理解这些概念和代码。

对象崇拜 (Object Fetishism)

"对象崇拜"这一术语指的是,当所有你能命名和操作的事物都被视为对象时 ,你会将一切都看作是"对象"。这意味着在面向对象编程(OOP)中,我们倾向于将现实世界的事物建模为对象,而忽视了它们可能只是 或其他更抽象的结构。

代码示例:

cpp 复制代码
struct gesichtbuch { 
    struct person { 
        id_t id; 
        std::string name; 
        std::string email; 
        std::string phone; 
        int birth_year; 
    }; 
    std::unordered_map<id_t, person> people; 
    boost::multi_index<std::pair<id_t, id_t>, ...> friendships; 
};

这里定义了一个 gesichtbuch 结构体,它包含了一个 person 结构体和两个数据成员:

  • people 是一个哈希表,用来存储 person 对象,每个 person 对象通过一个 id_t 唯一标识。
  • friendships 是一个多索引容器,用来存储人之间的友谊关系。
    这种设计是典型的面向对象编程(OOP)的风格,其中数据被封装在 对象 中,并通过不同的 方法 进行操作。

操作规范 (Operational Specs)

"操作规范"是对程序行为的详细描述,通常用于定义程序的操作和它们如何实现。它包括以下几种不同的描述方法:

  • 纯函数 (Pure Functions) : 纯函数是指不改变外部状态、并且相同的输入总是返回相同的输出的函数。例如:f(x) = x^2
  • 语义规范 (Denotational Specs): 通过对程序行为的数学定义或符号描述,来指定程序的行为。
  • 过程 (Procedures): 描述程序的步骤和算法。
  • 共享可变状态 (Shared Mutable State): 程序中多个部分可以同时修改的状态,通常在多线程环境中很重要。

程序设计的不同范式:

  1. 值和引用 (Reference and Value) :
    • 值传递 (Pass by Value):传递的是数据的副本,每次传递的值独立于其他地方的值。
    • 引用传递 (Pass by Reference):传递的是数据的引用,允许在函数内修改外部变量。
  2. 抽象 (Abstraction) :
    • 抽象是通过隐藏复杂性并提供简洁接口来简化编程的过程。它允许我们专注于问题的核心,而不必关心实现的细节。
  3. 语义 (Semantic) :
    • 语义指的是程序中各种元素的意义,比如数据类型、操作、控制结构等。程序的语义决定了它的执行效果。

OOP vs FP:

  1. 面向对象编程 (Object-Oriented Programming) :
    • 关注将数据和操作封装在对象中,通过对象与对象之间的交互来实现功能。
    • 是封装数据和行为的基本单元。
    • 继承多态 是 OOP 的核心特性,它们允许我们基于现有的对象创建新对象并重用行为。
  2. 函数式编程 (Functional Programming) :
    • 侧重于使用纯函数不可变值进行编程,避免副作用。
    • 函数是程序的核心,通过组合函数来解决问题。

编程工具与特性:

  • shared_ptr : 这是 C++ 中的智能指针之一,负责自动管理内存,避免内存泄漏。
  • mutex: 用于在多线程环境中同步对共享资源的访问,防止数据竞争。
  • constexpr : 一个 C++ 关键字,表示常量表达式,它可以在编译时计算出值,而不是在运行时计算。

总结:

  • 值和引用:编程语言中的值语义和引用语义,分别处理数据的不可变性和可变性。
  • 面向对象与函数式编程:面向对象编程 (OOP) 聚焦于对象和它们的交互,而函数式编程 (FP) 聚焦于函数的组合和不可变性。
  • 抽象与语义:编程中的抽象帮助简化问题的复杂性,而语义决定了程序的行为。

反思

这段代码展示了 C++ 中一些复杂的编程技巧和设计模式,涉及到了 std::shared_ptrstd::vector右值引用拷贝控制引用折叠等高级特性。我们逐一解析:

std::vector<int> push_back(std::vector<int> vec, int x)

cpp 复制代码
std::vector<int> push_back(std::vector<int> vec, int x) 
{ 
    vec.push_back(x); 
    return vec; 
}

这是一个简单的函数,接受一个 std::vector<int> 和一个整数 x,然后将 x 添加到向量的末尾,并返回更新后的向量。

  • 参数传递方式 :这里使用的是按值传递的方式,即复制了传入的 vec 向量。这意味着修改 vec 不会影响原始的向量(因为 vec 是按值传递的)。
  • 返回 :函数返回更新后的向量。由于返回值是一个局部变量,编译器通常会通过 返回值优化 (RVO) 来避免不必要的拷贝。

class foostd::shared_ptr

cpp 复制代码
class foo { 
    std::shared_ptr<impl> impl_;  // 使用std::shared_ptr来管理impl对象,确保impl对象的生命周期得到自动管理
public: 
    // const&版本的modified函数:该函数仅修改对象的内部状态(通过调用clone和mutate),而不改变对象本身
    foo modified(int arg) const& { 
        // 克隆impl对象,确保修改不影响原始的impl
        auto p = impl_->clone(); 
        // 修改克隆出来的impl对象
        p->mutate(arg); 
        // 返回一个新的foo对象,新的foo对象持有修改后的impl副本
        return foo{p}; 
    } 
    // &&版本的modified函数:该函数在对象是右值时被调用,允许原地修改当前对象
    foo&& modified(int arg) && { 
        // 检查impl是否是唯一的所有者(即当前对象是唯一拥有impl资源的)
        if (impl_->unique()) {  // unique()检查是否是唯一拥有者,不支持多线程安全
            // 如果是唯一所有者,直接修改impl对象
            impl_->mutate(arg); 
        }
        else {
            // 如果不是唯一所有者,表示还有其他shared_ptr引用impl对象
            // 这时通过克隆一个impl副本来避免共享修改的问题
            *this = modified();  // 使用左值版本的modified来创建新对象,并赋值给当前对象
        }
        // 返回当前对象的右值(std::move(*this)),允许调用者接管当前对象
        return std::move(*this); 
    } 
};

这个类展示了 C++11 中的 右值引用std::shared_ptr 的用法,特别是 "资源管理""拷贝与移动构造" 的设计。

1. std::shared_ptr<impl> impl_
  • impl_ 是一个 std::shared_ptr ,用来管理 impl 对象的生命周期。这里采用了 抽象实现 模式(即 pImpl ),在类 foo 中没有暴露具体的实现细节,只有一个指向 impl 的共享指针。
2. modified(int arg) const&
  • 这是一个 左值引用成员函数 ,表示它不会修改对象本身(const& 表示只读)。
  • 在该函数内部,调用了 impl_->clone() 来复制 impl 对象,然后通过 p->mutate(arg) 来修改复制出来的 impl
  • 最终,函数返回了一个新的 foo 对象,它持有这个新克隆的 impl,因此返回的是一个新对象而非修改现有对象。
3. modified(int arg) &&
  • 这是一个 右值引用成员函数 ,表示该函数是为了移动语义 设计的。只有当 foo 对象是右值时,才会调用这个函数。
  • impl_->unique() 判断 std::shared_ptr 是否是唯一的所有者。若是唯一所有者(即没有其他共享指针指向这个对象),则直接修改原始 impl_
  • 若不是唯一所有者,则通过重新调用 modified() 来克隆 impl,并返回更新后的对象。这种情况下,当前对象被赋值为一个新值,并最终返回右值(通过 std::move)。
cpp 复制代码
#include <iostream>
#include <memory>
// 假设impl是一个内部实现类
class impl {
public:
    int data;  // 模拟的内部数据
    impl(int val) : data(val) {}
    // 克隆方法,返回impl对象的副本
    std::shared_ptr<impl> clone() const { return std::make_shared<impl>(*this); }
    // 修改内部数据
    void mutate(int arg) { data += arg; }
    // 打印impl的数据
    void print() const { std::cout << "impl data: " << data << std::endl; }
};
// foo类管理一个impl对象
class foo {
    std::shared_ptr<impl>
        impl_;  // 使用std::shared_ptr来管理impl对象,确保impl对象的生命周期得到自动管理
public:
    // 构造函数,初始化impl对象
    explicit foo(std::shared_ptr<impl> impl) : impl_(std::move(impl)) {}
    // const&版本的modified函数:该函数仅修改对象的内部状态(通过调用clone和mutate),而不改变对象本身
    foo modified(int arg) const& {
        // 克隆impl对象,确保修改不影响原始的impl
        auto p = impl_->clone();
        // 修改克隆出来的impl对象
        p->mutate(arg);
        // 返回一个新的foo对象,新的foo对象持有修改后的impl副本
        return foo{p};
    }
    // &&版本的modified函数:该函数在对象是右值时被调用,允许原地修改当前对象
    foo&& modified(int arg) && {
        // 检查impl是否是唯一的所有者(即当前对象是唯一拥有impl资源的)
        if (impl_.use_count() == 1) {  // use_count()检查是否是唯一拥有者
            // 如果是唯一所有者,直接修改impl对象
            impl_->mutate(arg);
        } else {
            // 如果不是唯一所有者,表示还有其他shared_ptr引用impl对象
            // 这时通过克隆一个impl副本来避免共享修改的问题
            *this = modified(arg);  // 使用左值版本的modified来创建新对象,并赋值给当前对象
        }
        // 返回当前对象的右值(std::move(*this)),允许调用者接管当前对象
        return std::move(*this);
    }
    // 打印foo对象的impl数据
    void print() const { impl_->print(); }
};
int main() {
    foo f1(std::make_shared<impl>(10));  // 创建foo对象,impl的数据初始化为10
    f1.print();                          // 输出:impl data: 10
    // 使用左值版本修改foo对象的impl数据
    foo f2 = f1.modified(5);
    f2.print();  // 输出:impl data: 15
    // 使用右值版本修改foo对象的impl数据
    foo f3 = std::move(f1).modified(3);
    f3.print();  // 输出:impl data: 13
    // f1此时已被移动,不再可用(如果访问会导致未定义行为)
    return 0;
}

**class screen右值引用

cpp 复制代码
class screen { 
    screen_handle handle_; 
    screen(const screen&) = delete; 
public: 
    screen&& draw(...) && { 
        do_draw(handle_, ...); 
    } 
    auto draw(...) const { 
        return [hdl=handle_] (context ctx) { 
            do_draw(hdl, ...); 
        }; 
    } 
};

这个 screen 类展示了如何利用 右值引用 来控制对象的生命周期,同时也展示了 不可复制 的类设计模式。

1. screen_handle handle_;
  • handle_ 是类的核心成员,表示屏幕的句柄。它的类型是 screen_handle,可以是一个类或者指针,表示对某些资源(如屏幕)的引用。
2. screen(const screen&) = delete;
  • 这一行明确禁止了 拷贝构造函数 ,即不能复制 screen 对象。这是为了防止多个 screen 对象同时管理相同的资源(screen_handle),避免资源冲突或重复释放。
3. screen&& draw(...) &&
  • 这是一个 右值引用成员函数 ,表示该方法只能通过右值调用。这个函数执行某些 绘制操作 ,并且可能会修改或处理 handle_ 资源。
  • 右值引用允许将当前对象的状态传递给函数,通常用于当对象的生命周期即将结束时,允许它执行最后的操作(例如,绘制)。
4. auto draw(...) const
  • 这是一个 常量成员函数 ,它不会修改类的状态。它通过 闭包lambda)来返回一个可调用的函数,这个函数会在未来的某个时刻执行绘制操作。
  • hdl=handle_ 语法表示捕获 handle_ 的值,并且 do_draw(hdl, ...) 会使用它来绘制屏幕。

总结

  1. 右值引用 (&&) 在 C++ 中用于实现 移动语义 和优化资源管理。在 fooscreen 类中,右值引用被用来优化对象的移动和避免不必要的复制。
  2. std::shared_ptr 提供了智能指针,帮助管理资源生命周期。通过 unique() 方法,能够判断是否是唯一所有者,从而选择是否进行修改或复制。
  3. 禁用拷贝构造函数 (= delete) 用于防止对象复制,通常在资源管理类中使用,确保资源不会被不恰当地共享或重复释放。
  4. const 和右值引用的结合screen 类中的 const 函数返回了一个闭包,它允许我们不改变类的状态,但仍然能够按需执行某些操作。
    通过这些技巧,可以创建具有高效资源管理、灵活语义和优化性能的 C++ 代码。
cpp 复制代码
#include <iostream>
// 假设 screen_handle 和 context 是一些平台相关的类型
using screen_handle = int;  // 假设为整数类型,代表屏幕句柄
using context = int;        // 假设为整数类型,代表上下文(可以是绘图相关的对象)
// 假设 do_draw 是一个全局函数,用于在给定的屏幕句柄上绘制
void do_draw(screen_handle hdl, context ctx) {
    std::cout << "Drawing on screen with handle " << hdl << " in context " << ctx << std::endl;
}
// screen 类定义
class screen {
    screen_handle handle_;  // 屏幕句柄
    // 禁止拷贝构造函数
    screen(const screen&) = delete;
public:
    // 构造函数:初始化 screen_handle
    screen(screen_handle handle) : handle_(handle) {}
    // 右值引用版本的 draw
    screen&& draw(int ctx) && {
        // 在右值对象上直接执行绘制操作
        do_draw(handle_, ctx);    // 传递绘制上下文
        return std::move(*this);  // 返回右值
    }
    // 常量版本的 draw,返回一个 lambda 延迟执行绘制
    auto draw(int ctx) const& {  // 使用 & 来表示它是左值的
        return [hdl = handle_](context c) {
            // 使用屏幕句柄进行绘制
            do_draw(hdl, c);  // ctx 是由外部提供的上下文参数
        };
    }
};
// 测试代码
int main() {
    screen s(1);  // 创建一个 screen 对象,假设 1 是屏幕的句柄
    // 使用右值引用版本的 draw 函数
    std::move(s).draw(42);  // 42 作为绘制上下文
    // 使用常量版本的 draw 返回的 lambda
    auto lambda = s.draw(0);  // 0 作为绘制上下文
    lambda(42);               // 调用 lambda 并传递绘制上下文
    return 0;
}

"值语义(Value Semantics)" 以及 "对象(Object)" 的选择,通常与 宏观设计(Macro Design)微观设计(Micro Design) 的决策息息相关。我们可以从这些角度来理解何时使用 值语义对象

1. 对象 ⟶ 宏观设计(Macro Design)

宏观设计涉及系统架构的整体设计。对象通常用于 有状态 的实体,它们的状态会随时间变化。

  • 对象 是有状态的实体(例如:用户、文件、窗口等),你可以对它们的状态进行修改。
  • 何时使用对象(宏观设计)
    • 当需要 可变性:对象可以随时间改变其内部状态。
    • 当需要 复杂交互:多个对象之间需要进行交互,代表不同的概念(例如:用户、文件、任务等)。
    • 当需要管理 现实世界实体,这些实体的属性会随时间改变。
    • 当你需要使用 身份 来标识对象,而不仅仅是其数据。
      例子:
  • 在游戏中,玩家对象会有一个不断变化的得分、位置和生命值。
  • 在数据库管理中,每个记录对象可能具有不同的状态,随着数据库的更新其状态会变化。

2. 值 ⟶ 微观设计(Micro Design)

微观设计关注的是更小范围的设计决策,例如具体的数据结构和算法选择。值是 不可变 的,它代表不随时间变化的数据。值没有身份,通常用于不需要标识的数据处理。

  • 是不可变的。它们本身不具备身份(例如:一个数值、一段字符串)。
  • 何时使用值(微观设计)
    • 当需要 不可变性:值在创建后不可更改,避免了共享数据修改带来的副作用。
    • 当你需要 无状态的 组件,或者在 函数式编程 中更常见。
    • 当你只关心数据的 内容,而非其身份或是否发生改变(例如:一组不可变的数字、一个配置对象)。
    • 当你希望使用 值相等 比较(即数据相同的两个值视为相等,而不关心它们是否是同一个对象)。
      例子:
  • 数学向量:它的值是不可变的,两个相同的向量可以看作是相等的。
  • 字符串:如果我们把字符串作为值处理,它是不可变的,每次修改会返回一个新的字符串。

3. 值 ⟶ 宏观设计对象 ⟶ 微观设计

这些表达也可以理解为:

  • 值 ⟶ 宏观设计 :强调在系统设计的 大范围 结构中,使用不可变的值来传递数据和保持一致性。
    • 何时使用
      • 如果你的系统设计是 声明式的 ,或者偏向 函数式编程
      • 如果系统的主要关心点是 共享数据的安全性程序行为的可预测性
      • 当系统中主要是 抽象的数据传递 ,而不需要关注对象的状态和身份。
        例子:
    • 在函数式编程中,使用 持久化数据结构不可变数据结构 来表示系统的整体状态,而不是基于对象的状态。
    • 一个全局的 配置对象,它包含一些设置数据,整个系统中它是不可变的。
  • 对象 ⟶ 微观设计 :在 小范围 内部,使用 对象 来管理状态和操作。
    • 何时使用
      • 当你需要创建 自包含的单位,它们持有自己的状态并对其进行操作。
      • 当你需要利用 多态性继承 来定义不同类型对象的共享行为。
      • 当你关注 对象之间的细粒度交互 和对象的 状态管理
        例子:
    • 一个 银行账户对象,它有存款和取款的方法,以及当前余额的状态,操作这些方法会改变账户的状态。

4. 值 ⟶ 宏观设计对象 ⟶ 微观设计 的关系

这两者其实是 设计选择,你可以选择根据不同的设计目标来决定使用值还是对象。

  • 值 ⟶ 宏观设计:可以在系统的架构层面使用不可变数据结构(例如:数据流、函数式编程),适合那些需要高度声明性、避免副作用的场景。
  • 对象 ⟶ 微观设计:在小范围内,使用具有内部状态的对象来处理和管理数据。

总结:什么时候使用值语义(Value Semantics)?

  • 当你需要 不可变性,并希望通过不变数据来避免副作用时。
  • 当你在使用 函数式编程 ,尤其是需要 无状态的 组件时。
  • 当你关心 数据传递,而不关心数据的身份(即值相等,而不是对象相等)。
  • 当系统中的数据应保持一致性,避免修改操作时。
    例如:
  • 使用 不可变的集合持久化数据结构 来代表一些数据(例如:队列、栈、树等)。
  • 在一些需要 并发安全不可变对象 的系统中,使用 值语义 来确保数据不会被多个线程修改。

第二部分:面向值的交互软件架构

这一部分讨论的是一种 面向值的架构 ,用于 交互式软件 。在这种架构中,对象(Objects)引用(Reference)间接引用(Indirect Reference)模型(Model)控制器(CTRL)视图(View) 是关键的组成部分,帮助我们理解软件组件如何互相交互。

我们逐一分析这些术语,并解释它们如何适应 面向值的架构

1. 对象(Objects)

在面向对象设计(OOD)中,对象是基本的封装单元。它包含了:

  • 数据(属性或字段)
  • 行为 (方法或函数)
    面向值的架构 中,对象通常是不可变的 ,意味着一旦创建对象,它的状态就不能被改变。如果需要更改状态,将会创建一个新的对象实例,而不是直接修改原对象,这体现了 值语义(value semantics)。
示例:
  • 在一个游戏中,玩家(Player) 对象表示玩家的得分、生命值和名字。每当玩家得分时,不会修改原来的玩家对象,而是创建一个新对象,表示更新后的玩家状态。

2. 引用(Reference)

引用 指向对象的内存地址或位置,允许你访问或修改该对象。在 面向值的架构 中,引用的使用需要小心,以避免意外修改共享数据。

例如,当在 面向值的架构 中传递对象时,通常会使用 引用指针 ,但尽可能使用不可变对象。这有助于保持数据的 完整性,避免多线程环境下的修改冲突。

示例:
  • 如果你传递一个 Player 对象作为引用,接收方可以访问它,但如果对象是不可变的,就无法修改它,除非显式创建一个新的对象来表示更新。

3. 间接引用(Indirect Reference)

间接引用 指的是通过其他对象(通常是通过指针或智能指针)访问目标对象。这使得对象的内存地址或位置不会被直接暴露。

面向值的架构 中,通常会使用 间接引用 来避免直接修改原始对象的状态。通过智能指针(如 C++ 的 std::shared_ptr),可以在不同地方共享对象,但确保数据的一致性和不可变性。

示例:
  • 使用 shared_ptr 可以将同一个玩家对象传递给多个组件,但只有某个组件能够修改它。如果另一个组件需要修改它,就会通过克隆该对象来避免直接共享修改。

4. 模型(Model)

模型 表示应用程序的核心数据和业务逻辑。它封装了软件的主要概念(如数据实体或业务规则)。模型通常聚焦于表示现实世界的概念 (如用户、游戏状态、配置信息等)。

面向值的架构 中,模型通常采用 不可变的数据结构。当模型发生变化时,通常会创建新实例,而不是直接修改现有数据。

示例:
  • 在一个 待办事项(Todo) 应用中,模型可能代表一个任务列表,每个任务有名称、描述和状态。每个任务对象是不可变的,添加、完成或删除任务时,会生成一个新的任务列表实例,而不是直接修改原有列表。

5. 控制器(CTRL)

控制器 是架构中的部分,负责处理 用户输入 和管理 视图模型 之间的交互。控制器接收来自 视图 的事件(如点击、键盘输入),并根据这些事件更新 模型

面向值的架构 中,控制器通常充当 视图模型 之间的中介。由于模型是不可变的,控制器可能需要通过创建新的模型实例来更新模型,而不是直接修改现有模型。

示例:
  • 在一个游戏中,当玩家执行某个动作(如点击按钮)时,控制器会:
    • 接收输入事件
    • 创建一个新的游戏状态(表示该动作后的结果)
    • 更新视图,显示新的游戏状态。

6. 视图(View)

视图 表示应用程序的 用户界面(UI) 。它负责将 模型 中的数据呈现给用户,并根据模型的变化更新显示。

面向值的架构 中,视图通常会监听模型的变化,并根据新的模型状态重新绘制界面。由于模型是不可变的,每次模型变化时,视图会接收到新的模型实例来更新显示。

示例:
  • 视图 可能显示 玩家(Player) 的分数和生命值。当 控制器 更新玩家状态时,视图 会基于新的玩家对象来更新显示,而这个新对象代表的是玩家状态的更新版。

这些概念如何适应面向值的架构

面向值的架构 中,主要目标是保持 数据不可变性 (通过值语义)并清晰地划分 模型控制器视图 的责任:

  • 对象 存储不可变的数据。
  • 引用间接引用 允许访问对象,但由于对象是不可变的,因此避免了无意的修改。
  • 模型 以干净的、不可变的方式封装数据,便于状态管理和变更跟踪。
  • 控制器 处理业务逻辑和用户输入,通过创建新的模型实例而非修改现有模型来更新模型。
  • 视图 根据新的模型状态更新 UI,总是显示最新的、不可变的模型数据。

总结

面向值的架构 中,不可变数据 (值语义)和 关注分离 的设计原则紧密结合,有助于避免因共享可变状态所导致的复杂性和潜在错误。这种架构使得软件更加 可预测易于维护,尤其在复杂的交互式软件中,可以显著提高可靠性和可扩展性。

单向数据流架构(Unidirectional Data Flow Architecture)

单向数据流架构 (Unidirectional Data Flow Architecture) - 对象引用与间接引用

在你提供的 Mermaid 图中,我们看到了 单向数据流架构 的典型模式。这个图展示了 Action (动作)到 Model (模型),然后再到 View (视图)的数据流动过程,并且在 ModelView 之间还有一定的交互。

让我们来一步步解释图中的内容:
update(model, action) → model draw(model)
render(model) → view dispatch(action) ACTION MODEL VIEW

复制代码
  graph TD
      A[ACTION] -->|"update(model, action) → model"| B[MODEL]
      B -->|"draw(model) <br> render(model) → view"| C[VIEW]
      B -.-> B
      C -.->|"dispatch(action)"| A
      style A fill:#552299,stroke:#333,stroke-width:2px
      style B fill:#225522,stroke:#333,stroke-width:2px
      style C fill:#882211,stroke:#333,stroke-width:2px
图解:
  1. ACTION → MODEL :
    • Action 是触发的用户行为或外部事件,作用是影响 Model(模型)的状态。
    • Action 发出后,模型被更新。图中表示为 update(model, action) → model。这通常是通过 Action Creator 或类似机制进行的,模型在这一步骤中发生变化。
  2. MODEL → VIEW :
    • Model 更新后,视图需要渲染这些变化。图中 draw(model) <br> render(model) → view 表示从 Model 获取数据并渲染到 View(视图)上。这个步骤通常是 UI 更新的一部分,例如 React 中的重新渲染。
  3. 间接引用 - B -.-> B :
    • 这里的 B -.-> B 代表 Model 的状态可能会发生 间接引用 ,例如 Model 可能会根据某些条件内部更新(自我修改)或自我触发某些操作。这是 间接引用 的一种形式,其中 Model 根据其内部状态或外部信号进行自我更新。
  4. VIEW → ACTION :
    • ViewAction 之间的 间接引用 :图中显示了 C -.->|"dispatch(action)"| A,意味着 View 可以通过与用户的交互,发出新的 Action ,然后再次通过 Action 更新 Model,形成一个新的循环。这个动作可以是用户点击按钮、输入文本等。

对象引用与间接引用:

单向数据流架构 中,提到的 对象引用间接引用 主要是指在架构中的对象(如 ModelView)之间的关系。

  1. 对象引用(Reference)
    • 对象引用意味着我们直接持有对某个对象的引用或指针。例如,如果 View 直接引用了 Model ,那么视图可能会直接操作模型的数据或结构。在图中,如果 View 直接访问 Model ,就表现为一个直接的引用(A → B)。
  2. 间接引用(Indirect Reference)
    • 间接引用 表示我们并没有直接持有对象的引用,而是通过某种中介或者间接方式来操作对象。例如,Model 内部的自我更新或 View 通过触发 Action 来间接影响 Model 。在图中,B -.-> BC -.-> A 就代表了间接引用的行为,说明对象之间并不是通过直接的引用进行交互,而是通过某些中介或触发器来达成影响。

总结:

  1. 单向数据流架构 通过明确的数据流动方向,确保了数据和操作的清晰和一致性。数据总是从 ActionModel ,再从 ModelView,避免了不必要的复杂交互和混乱。
  2. 对象引用间接引用 是该架构中的两个重要概念。直接引用指的是对象之间直接访问,而间接引用则是通过某些中介或事件触发来间接影响对象。
    这种架构使得开发者能够更好地理解数据是如何在应用中流动的,并减少了因复杂交互带来的调试难题。

这个例子展示了一个 交互式计数器 (Interactive Counter)模型。通过用户输入的事件(例如 "+"、"-"、".")来更新计数器的值,并且每次更新后会显示当前值。这个例子展示了如何使用 单向数据流架构 来设计和实现交互式应用。

代码结构解析

  1. 数据模型 (model) :

    cpp 复制代码
    struct model { 
        int value = 0; 
    };
    • 这是一个简单的 model 结构体,表示计数器的状态,它只有一个成员 value 来存储当前计数器的值,初始化为 0
  2. 动作类型 (increment_action, decrement_action, reset_action) :

    cpp 复制代码
    struct increment_action {}; 
    struct decrement_action {}; 
    struct reset_action { int value = 0; }; 
    • increment_action:表示增加计数的动作。
    • decrement_action:表示减少计数的动作。
    • reset_action :表示重置计数器的动作,带有一个可选的 value,可以指定重置后的值(默认为0)。
  3. 更新函数 (update) :

    cpp 复制代码
    model update(model c, action action) { 
        return std::visit(lager::visitor{ 
            [&] (increment_action) { 
                return model{ c.value + 1 }; 
            }, 
            [&] (decrement_action) { 
                return model{ c.value - 1 }; 
            }, 
            [&] (reset_action a) { 
                return model{ a.value }; 
            }
        }, action); 
    }
    • update 函数根据传入的 action 类型来更新 model 。它使用了 std::visitvisitor 模式来对不同的动作(increment_actiondecrement_actionreset_action)进行处理。
    • 如果是 increment_action,则将当前 value 增加1;如果是 decrement_action,则将当前 value 减少1;如果是 reset_action,则将 value 设置为指定的值(如果没有指定值则默认为 0)。
  4. 绘制函数 (draw) :

    cpp 复制代码
    void draw(counter::model c) { 
        std::cout << "current value: " << c.value << '\n'; 
    }
    • draw 函数用于在终端输出当前计数器的值。每次计数器值变化后都会调用此函数来显示新的状态。
  5. 事件解析函数 (intent) :

    cpp 复制代码
    std::optional<action> intent(char ev) { 
        switch (ev) { 
        case '+': 
            return increment_action{}; 
        case '-': 
            return decrement_action{}; 
        case '.': 
            return reset_action{}; 
        default: 
            return std::nullopt; 
        } 
    }
    • intent 函数根据用户输入的字符 ev 返回相应的 action 类型。
    • 如果用户输入 +,返回 increment_action;如果输入 -,返回 decrement_action;如果输入 .,返回 reset_action;如果输入的是其他字符,则返回 std::nullopt 表示无效输入。
  6. main 函数 :

    cpp 复制代码
    int main() { 
        auto state = counter{0}; 
        auto event = char{}; 
        while (std::cin >> event) { 
            if (auto act = intent(event)) { 
                state = update(state, *act);
                draw(state); 
            } 
        } 
    }
    • state :表示当前计数器的状态,初始值为 0
    • event:表示用户输入的事件(字符)。
    • while 循环中,程序不断从标准输入读取字符 event,并根据输入解析成相应的 action 。如果有有效的 action ,则调用 update 函数来更新 model ,然后调用 draw 函数显示当前的计数器状态。

总结

这个交互式计数器应用演示了一个简单的 单向数据流架构

  1. 用户通过输入触发 Action (例如 + 增加,- 减少,. 重置)。
  2. Action 会传递给 Model ,并通过 update 函数更新 Model 的状态。
  3. Model 更新后,视图通过 draw 函数显示新的计数值。
核心设计:
  • 状态(Model) 只通过 Action 更新,而不是直接从视图中修改。
  • Action 是用户交互的触发器(例如按键),通过 intent 函数解析。
  • 更新和绘制 是分开的,符合 单向数据流 的设计模式。

交互流

  • 用户输入 → 解析事件 → 生成 Action → 更新 Model → 绘制更新后的 Model

优点

  • 使代码易于理解、测试和维护。
  • 清晰的 数据流事件处理 机制。
cpp 复制代码
#include <iostream>
#include <variant>
#include <optional>
// 定义计数器的状态模型
struct model {
    int value = 0;  // 当前计数器的值
};
// 定义所有的操作(Action)
struct increment_action {};  // 增加计数
struct decrement_action {};  // 减少计数
struct reset_action {        // 重置计数
    int value = 0;           // 重置后的值
};
// 定义一个`action`类型,它可以是上面定义的任何一种动作
using action = std::variant<increment_action, decrement_action, reset_action>;
// 更新函数,根据动作更新模型
model update(model c, action act) {
    return std::visit(
        [&](auto&& action) -> model {
            if constexpr (std::is_same_v<std::decay_t<decltype(action)>, increment_action>) {
                return model{c.value + 1};  // 增加计数
            } else if constexpr (std::is_same_v<std::decay_t<decltype(action)>, decrement_action>) {
                return model{c.value - 1};  // 减少计数
            } else if constexpr (std::is_same_v<std::decay_t<decltype(action)>, reset_action>) {
                return model{action.value};  // 重置计数
            }
        },
        act);  // 使用std::visit处理variant
}
// 绘制函数,输出当前计数器值
void draw(const model& m) { std::cout << "current value: " << m.value << '\n'; }
// 事件解析函数,根据输入的字符生成相应的Action
std::optional<action> intent(char ev) {
    switch (ev) {
        case '+':
            return increment_action{};  // 增加
        case '-':
            return decrement_action{};  // 减少
        case '.':
            return reset_action{};  // 重置
        default:
            return std::nullopt;  // 无效输入
    }
}
// 主函数
int main() {
    model state;  // 初始模型,默认值为0
    char event;   // 用于存储输入的事件字符
    std::cout << "Interactive Counter\n";
    std::cout << "Press '+', '-', or '.' to interact.\n";
    // 无限循环,等待用户输入
    while (std::cin >> event) {
        // 解析输入的事件
        if (auto act = intent(event)) {
            // 更新模型并绘制结果
            state = update(state, *act);
            draw(state);  // 输出当前计数器值
        } else {
            std::cout << "Invalid action. Try again.\n";
        }
    }
    return 0;
}

根据提供的代码,我将为你绘制一个 Mermaid 模型图。此图展示了 交互式计数器 的整体架构,包括模型(model)、动作(action)、更新(update)和视图(draw)之间的关系。

Mermaid 图:

intent(event) → action update(model, action) → model draw(model) → output event input (e.g., '+', '-', '.') ACTION MODEL VIEW

Mermaid 图的解释

  1. ACTION :
    • 用户输入的字符(如 '+', '-', '.')通过 intent(event) 被转换为对应的动作(increment_actiondecrement_actionreset_action)。intent 函数解析事件并返回一个可选的动作类型。
  2. MODEL :
    • model 存储当前计数器的值,并根据不同的 ACTION 更新其状态。update(model, action) 函数会根据 action 类型来更新模型的值(加 1、减 1 或重置为指定的值)。这种方式通过 std::visit 动态调度来根据不同的 action 类型执行不同的更新。
  3. VIEW :
    • VIEW 是输出层,负责展示当前的 model 值。draw(model) 函数会将 model 中存储的 value 输出到屏幕上。
  4. 单向数据流 :
    • 数据流从 ACTION 开始,由用户的输入触发,通过 intent(event) 生成 ACTION
    • 然后 MODEL 被更新,通过 update(model, action) 修改。
    • VIEW 负责将模型中的数据绘制到输出中(即输出当前计数器值)。
    • 最后,视图通过用户的输入触发新的 ACTION,从而进入一个循环。

颜色和样式说明:

  • ACTION(浅粉色)表示用户输入的操作。
  • MODEL(浅绿色)表示存储计数器状态的模型。
  • VIEW (浅蓝色)表示将模型数据渲染到屏幕上的输出层。
    这种设计体现了 单向数据流 架构,确保了程序的清晰和可维护性。
    你提供的代码和内容涉及了多种编程范式和概念,尤其是在 函数式编程响应式编程 上。下面是对代码片段和相关概念的解释:

1. Elm, Redux 和 Lager

  • Elm : 这是一个函数式编程框架,用于构建前端应用。它基于 Haskell ,并使用 单向数据流不可变数据结构 来管理状态更新和渲染。
  • Redux: 是 JavaScript 中用于管理应用状态的一个库,采用单向数据流的架构。它通过一个"store"来保存应用状态,并通过"action"来描述如何更新这个状态。
  • Lager: 类似于 Redux,但它是为 C++ 语言设计的。它使得 C++ 应用能够像 Redux 一样管理状态,使用类似的架构和理念。

2. draw, intent, main 函数中的工作原理

  • draw(model c):绘制函数,它用于将当前模型的状态(例如计数器的值)输出到屏幕。
  • intent(event_t ev):处理事件的函数,它将来自用户输入(例如按键)转换成相应的"动作"(Action)。
  • main() :是程序的入口点,其中设置了一个 Lager store 来管理应用状态,并通过事件监听来更新模型,最后调用 draw 函数来渲染新的状态。

3. 代码分析

cpp 复制代码
int main() { 
    auto debug = lager::http_debug_server{8080}; 
    auto serv  = boost::asio::io_service{}; 
    auto store = lager::make_store<action>( 
        counter{0}, 
        update, 
        draw, 
        lager::boost_asio_event_loop{serv}, 
        lager::enable_debug(debug)); 
    auto term  = ncurses::terminal{serv}; 
    term.listen([&] (event_t ev) { 
        store.dispatch(intent(ev)); 
    }); 
    serv.run(); 
}
  • lager::make_store :创建了一个类似 Redux 的"store",它包含了模型的初始状态 counter{0},状态更新函数 update,渲染函数 draw,以及一个事件循环(基于 Boost Asio)。
  • term.listen() :监听终端输入事件,用户的输入(例如按键)会被转换为 action ,然后 store.dispatch() 将此 action 分发给 store 来更新模型。
  • serv.run():启动事件循环,监听输入并进行处理。

4. debugger 模型

cpp 复制代码
template <typename Action, typename Model> 
struct debugger { 
  struct goto_action { std::size_t cursor; }; 
  struct undo_action {}; 
  struct redo_action {}; 
  using action = std::variant< 
      Action, 
      goto_action, 
      undo_action, 
      redo_action>;  // 动作类型包括常规动作和调试相关动作(跳转,撤销,重做)
  struct model { 
    Model init; 
    std::size_t cursor = {};  // 当前操作指针
    immer::vector<std::pair<Action, Model>> 
        history = {};  // 操作历史
    model(Model i) : init{i} {} 
    operator const Model& () const { 
        return cursor == 0 
          ? init : history[cursor - 1].model;  // 返回当前模型的状态(包括历史)
    }
  };
}
  • debugger 模型 :用于记录和调试应用的历史状态。它不仅保存当前模型的状态(init),还维护一个历史记录(history)以便进行撤销(undo)、重做(redo)等操作。
  • goto_action, undo_action, redo_action:这些是调试功能相关的动作。通过它们可以在历史状态中跳转、撤销或重做。
  • model :这个结构包含了应用的当前状态、操作指针(cursor)和历史记录(history)。它通过 operator const Model& 实现了历史记录的访问。

5. update 函数

cpp 复制代码
model update(model m, action act, std::function<Model(Model, Action)> reducer) { 
    return std::visit(visitor{ 
        [&] (Action act) { 
            ... reducer(m, act) 
        }, 
        [&] (goto_action act) { 
            ... 
        }, 
        [&] (undo_action) { 
            ... 
        }, 
        [&] (redo_action) { 
            ... 
        }, 
    }, act); 
}
  • update 函数 :这个函数接收一个模型和一个动作(action),并使用 std::visit 来调度处理不同类型的动作(例如 Action, goto_action, undo_action, redo_action)。对于每个动作,执行不同的更新逻辑。

6. effectupdate

cpp 复制代码
template <typename Action> 
using effect = std::function<void(context<Action>)>; 
pair<model, effect<action>> update(model, action);
  • effect :它是一个函数类型,用于描述模型更新时的副作用。effect<Action> 接受一个 context<Action>,并返回一个没有返回值的函数(即副作用函数)。副作用通常用于异步操作、I/O 操作等场景。
  • update :更新函数返回一个 model 和一个 effect。这表明更新不仅可以改变模型的状态,还可能执行一些副作用。

7. Postmodern Immutable Data Structures

  • Postmodern Immutable Data Structures (后现代不可变数据结构)指的是使用不可变数据结构的编程范式。不可变数据结构具有以下特点:
    • 不可修改:一旦创建,数据结构的内容不能被修改。
    • 高效的更新:通过结构共享和惰性复制,可以高效地更新不可变数据结构。
    • 函数式编程的核心:不可变数据结构使得函数式编程成为可能,它通过消除副作用和共享状态来简化程序的理解和调试。

总结:

这个代码实现了一个基于 LagerBoost Asio 的交互式计数器应用,它结合了函数式编程和响应式编程的思想,使用了 不可变数据结构单向数据流 的架构。同时,它也展示了如何处理撤销和重做等调试功能,通过对历史操作的管理来实现。

cpp 复制代码
#include <iostream>
#include <variant>
#include <optional>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
// 定义计数器的状态模型
struct model {
    int value = 0;  // 当前计数器的值,初始为0
};
// 定义所有的操作(Action)
struct increment_action {};  // 增加计数操作
struct decrement_action {};  // 减少计数操作
struct reset_action {        // 重置计数操作
    int value = 0;           // 重置后的值
};
// 定义一个`action`类型,它可以是上面定义的任何一种动作
using action = std::variant<increment_action, decrement_action, reset_action>;
// 更新函数,根据动作更新模型
model update(model c, action act) {
    // 使用std::visit来处理不同类型的action
    return std::visit(
        [&](auto&& action) -> model {
            if constexpr (std::is_same_v<std::decay_t<decltype(action)>, increment_action>) {
                return model{c.value + 1};  // 增加计数
            } else if constexpr (std::is_same_v<std::decay_t<decltype(action)>, decrement_action>) {
                return model{c.value - 1};  // 减少计数
            } else if constexpr (std::is_same_v<std::decay_t<decltype(action)>, reset_action>) {
                return model{action.value};  // 重置计数
            }
        },
        act);  // 使用std::visit处理variant
}
// 绘制函数,输出当前计数器值
void draw(const model& m) { 
    std::cout << "current value: " << m.value << '\n'; 
}
// 事件解析函数,根据输入的字符生成相应的Action
std::optional<action> intent(char ev) {
    switch (ev) {
        case '+':
            return increment_action{};  // 增加
        case '-':
            return decrement_action{};  // 减少
        case '.':
            return reset_action{};  // 重置
        default:
            return std::nullopt;  // 无效输入,返回空
    }
}
// 简单的事件循环类事件调度
class event_loop {
public:
    event_loop() : running(true) {}
    // 启动事件循环
    void run() {
        while (running) {
            // 在条件变量上等待,直到队列不为空或事件循环停止
            std::unique_lock<std::mutex> lock(mutex);
            cond_var.wait(lock, [this]() { return !event_queue.empty() || !running; });
            if (!event_queue.empty()) {
                // 从队列中获取事件
                char ev = event_queue.front();
                event_queue.pop();
                lock.unlock();  // 解锁,允许其他线程操作队列
                process_event(ev);  // 处理事件
            }
        }
    }
    // 添加事件到队列
    void dispatch(char ev) {
        std::lock_guard<std::mutex> lock(mutex);  // 确保线程安全
        event_queue.push(ev);  // 将事件加入队列
        cond_var.notify_one();  // 通知等待中的事件循环线程
    }
    // 停止事件循环
    void stop() {
        running = false;
        cond_var.notify_all();  // 唤醒所有线程,结束事件循环
    }
private:
    // 处理队列中的事件
    void process_event(char ev) {
        if (auto act = intent(ev)) {  // 判断输入是否为有效事件
            // 更新模型并绘制结果
            state = update(state, *act);
            draw(state);  // 输出当前计数器值
        } else {
            std::cout << "Invalid action. Try again.\n";  // 输入无效,提示用户
        }
    }
    bool running;  // 控制事件循环是否运行
    std::queue<char> event_queue;      // 事件队列,存储待处理的事件
    std::mutex mutex;                  // 互斥锁,确保线程安全
    std::condition_variable cond_var;  // 条件变量,用于线程间通信
    model state;                       // 当前模型,存储计数器的状态
};
// 主函数
int main() {
    event_loop loop;  // 创建事件循环对象
    // 启动事件循环的线程
    std::thread loop_thread([&]() { 
        loop.run();  // 在新线程中运行事件循环
    });
    // 模拟监听终端输入
    char event;
    std::cout << "Interactive Counter\n";
    std::cout << "Press '+', '-', or '.' to interact.\n";
    // 无限循环,等待用户输入并将事件传递给事件循环
    while (std::cin >> event) {
        loop.dispatch(event);  // 将事件添加到事件队列中
    }
    loop.stop();         // 停止事件循环
    loop_thread.join();  // 等待事件循环线程结束
    return 0;
}

注释说明:

  1. 计数器模型(model :
    • 定义了一个 model 结构体,表示当前计数器的状态,包含一个 int value,其初始值为 0。
  2. Action 类型(action :
    • 定义了三种操作(increment_action, decrement_action, reset_action),它们分别代表增加、减少和重置计数器值。
    • 使用 std::variant 将这三种操作封装成一个 action 类型。
  3. 更新模型(update :
    • update 函数根据传入的 action 更新计数器的值。它使用 std::visit 来处理不同类型的 action
  4. 绘制函数(draw :
    • draw 函数用于输出当前计数器的值。
  5. 事件解析(intent :
    • intent 函数根据用户输入的字符返回对应的 action。若输入无效,返回 std::nullopt
  6. 事件循环(event_loop :
    • event_loop 类负责管理事件队列,并在后台运行一个事件循环,等待并处理事件。
    • 通过 std::mutexstd::condition_variable 来实现线程间同步,保证事件循环的线程安全。
  7. 主函数 :
    • main 函数中,创建一个 event_loop 对象,并启动一个独立的线程来执行事件循环。
    • 主线程用于监听用户输入,将事件通过 loop.dispatch 方法传递到事件循环中。
    • 当用户输入字符后,事件被处理并更新计数器状态,最后输出当前值。

关键点:

  • 事件驱动: 使用 event_loop 类来模拟事件循环,处理用户输入并更新模型状态。
  • 线程和同步: 使用 std::mutexstd::condition_variable 来同步多个线程之间的事件处理。
相关推荐
weixin_4373982112 分钟前
转Go学习笔记(2)进阶
服务器·笔记·后端·学习·架构·golang
jyan_敬言27 分钟前
【C++】string类(二)相关接口介绍及其使用
android·开发语言·c++·青少年编程·visual studio
慕y27433 分钟前
Java学习第十六部分——JUnit框架
java·开发语言·学习
liulilittle1 小时前
SNIProxy 轻量级匿名CDN代理架构与实现
开发语言·网络·c++·网关·架构·cdn·通信
Shartin1 小时前
CPT208-Human-Centric Computing: Prototype Design Optimization原型设计优化
开发语言·javascript·原型模式
peace..1 小时前
温湿度变送器与电脑进行485通讯连接并显示在触摸屏中(mcgs)
经验分享·学习·其他
dme.1 小时前
Javascript之DOM操作
开发语言·javascript·爬虫·python·ecmascript
teeeeeeemo1 小时前
回调函数 vs Promise vs async/await区别
开发语言·前端·javascript·笔记
加油吧zkf1 小时前
AI大模型如何重塑软件开发流程?——结合目标检测的深度实践与代码示例
开发语言·图像处理·人工智能·python·yolo
tan77º1 小时前
【Linux网络编程】Socket - UDP
linux·服务器·网络·c++·udp