【Rust自学】15.6. RefCell与内部可变性:“摆脱”安全性限制

题外话,这篇文章一共4050字,是截止到目前为止最长的文章,如果你能坚持读完并理解,那真的很强!
请添加图片描述
喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
请添加图片描述

15.6.1. 什么是内部可变性

内部可变性(interior mutability)是Rust的设计模式之一,它允许程序员在只持有不可变引用的前提下对数据进行修改。

通常而言,这样的行为会被借用规则(详见 4.4. 引用与借用)所禁止,但是为了能够改变数据,内部可变性模式在代码的数据结构里使用了unsafe代码来绕过Rust正常的可变性和借用规则。

不安全代码向编译器表明我们正在手动检查规则,而不是依赖编译器为我们检查规则。不安全代码相关的概念将在以后的文章中涉及。

15.6.2. RefCell<T>

Rc<T>不同,RefCell<T>类型代表了其持有数据的唯一所有权

为了了解RefCell<T>Box<T>的区别,我们得回顾一下借用规则(详见 4.4. 引用与借用):

  • 在任何给定时间,你可以拥有(但不能同时拥有)一个可变引用或任意数量的不可变引用。
  • 引用总是保持有效。

PS:给定时间可以理解为给定的作用域内

RefCell<T>Box<T>的区别如下:

类型检查阶段规则违背后果
Box<T>编译阶段检查借用规则编译时报错
RefCell<T>运行时检查借用规则触发 panic

借用规则在不同阶段进行检查有不同的特点:

  • 编译阶段:

    • 尽早暴露问题
    • 没有任何运行时的开销
    • 是大多数场景的最佳选择
    • 是Rust的默认行为
  • 运行时:

    • 问题暴露延后,甚至到生产环境
    • 因借用计数产生些许性能损失
    • 实现某些特定的内存安全场景(比如在不可变环境中修改自身数据)

该在什么时候使用`RefCell

Rust编译器在编译阶段会检查所有的代码,其中大部分代码它都能够分析明白,如果没有问题就通过编译,如果有问题就报错。

Rust编译器是非常保守的,某些代码并不能在编译阶段就能分析明白,针对这类无法在编译阶段完成分析的代码Rust会直接拒绝掉,哪怕这些代码本质上没有任何问题。

Rust这么保守是为了保证程序的安全性。虽然拒绝掉某些本身没有问题的代码会对开发者造成不便,但是至少不会产生任何灾难性的后果。

针对这些编译器无法分析的代码,如果开发者能够保证这段代码满足借用规则,那么就可以使用RefCell<T>

RefCell<T>类似,Rc<T>只适用于单线程场景。

15.6.3. 如何在Box<T>Rc<T>RefCell<T>中进行选择

根据下表列出的三者的特性就可以进行选择:

特性Box<T>Rc<T>RefCell<T>
同一数据的所有者一个多个一个
可变性、借用检查可变、不可变借用(编译时检查)不可变借用(编译时检查)可变、不可变借用(运行时检查)

额外说一句,由于RefCell<T>在运行时才会被检查,所以即使RefCell<T>本身是不可变的,但我们仍然可以修改里面储存的值

15.6.4. 内部可变形:可变的借用一个不可变的值

这个小标题有一点绕,意思是对一个没有声明为mut的类型使用&mut引用。看个例子就明白了:

fn main() {
    let x = 5;
    let y = &mut x;
}

借用规则的一个推论是,当你有一个不可变的值时,你就不能可变地借用它。所以这么写会报错:

error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
 --> src/main.rs:3:13
  |
3 |     let y = &mut x;
  |             ^^^^^^ cannot borrow as mutable
  |
help: consider changing this to be mutable
  |
2 |     let mut x = 5;
  |         +++

For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` (bin "borrowing") due to 1 previous error

然而在某些特定情况下,我们会需要这样一个值——它对外部保持不可变,但它同时能在方法内部修改自身的值,除了这个值本身的方法,其余的代码都不能修改这个值,这叫做内部可变性RefCell<T>就是为了这种情况而存在的。

但是RefCell<T>并没有完全地绕开借用规则,编译阶段的检查虽然能够通过,但是在运行阶段如果违反了借用规则就会造成程序恐慌。

下面看一个例子(lib.rs):

功能:用于跟踪某个值与最大值的接近程度,并在该值达到特定级别时发出警告

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

这个例子的逻辑并不重要,看一下它的写法:

  • 程序开头定义了Messenger trait,里面有send方法的签名:接收不可变引用&self和一个字符串切片类型&str的形参msg作为参数。

  • 下面定义了一个结构体叫LimitTracker,它是一个泛型类型,生命周期为'a,泛型参数为T,要求T的生命周期为'a并实现Messenger这个在程序开头定义的trait。LimitTracker里面有三个字段:

    • messenger:类型为&str字符串切片类型,生命周期为'a
    • value:类型为usize
    • max:类型为usize
  • 往下看,通过impl块在LimitTracker上写了关联函数new,其参数是类型为泛型引用&T的形参messenger和类型为usize的形参max,返回值是LimitTracker类型。这个函数用于创建LimitTracker实例,这个实例:

    • messenger字段是形参menssenger的值
    • value字段值为0
    • max字段值为形参max的值
  • LimitTracker还有一个方法叫做set_value,其第一个参数是self的可变引用&mut self,第二个参数是value,类型为usize
    方法内部的代码逻辑很简单。把selfvalue字段值和参数value的值相除(还要转换成f64避免丢失精度)得到一个百分比,存储在percentage_of_max内。根据percentage_of_max的大小使用Messenger trait下的send方法发送不同的警告。

使用测试替代(test double)进行测试

这里有一个问题,如果我们要对这个set_value方法进行测试,就需要这个方法得输出些什么东西以供断言。但是set_value方法实际上并没有返回任何的值,所以说它不会提供任何的结果来进行断言。

我们要测试的是当某一个实现了Messenger trait的值和一个max值来创建LimitTracker实例时,传入不同的value就能够触发Messenger发送不同的消息。

为了解决这个问题,这里要介绍test double,它的中文叫测试替代,是一个通用的变成概念,代表了测试工作中被用作其他类型的替代品。test double中有一个特定的类型,叫模拟对象Mock Object),它会承担记录测试过程中的工作。我们就可以利用这些记录来断言这个测试工作运行是否正确。

Rust里没有类似的概念,在标准库里也没有模拟对象(Mock Object),但是我们可以自定义一个结构体来实现和Mock Object相同的功能。

接着上文的代码来写:

#[cfg(test)]
mod tests {
    use super::*;

    struct MockMessenger {
        sent_messages: Vec<String>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: vec![],
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.len(), 1);
    }
}
  • 在测试模块的最开头先声明了MockMessenger结构体,里面有1个字段sent_message,表示发送的消息,其类型是Vec<String>

  • MockMessenger在下文又通过impl块创建了了new函数用于创建MockMessenger的实例,实例的sent_messages字段的值是一个空的Vector

  • 后面又为MockMessenger结构体实现了整个代码一开头的Messenger trait。实现了这个trait之后MockMessenger就可以用来创建LimitTracker(因为LimitTracker要求泛型类型实现Messenger trait)。
    使用send方法时这个消息会存储在MockMessenger下字段sent_message这个Vector里。

  • 最后是it_sends_an_over_75_percent_warning_message这个测试函数,它测试的是超过75%的这部分。
    首先创建了MockMessenger的实例叫mock_messenger,然后创建了一个LimitTracker的实例叫limit_tracker,接着在LimitTracker的实例上(就是limit_tracker)调用。
    最后通过mock_messengersent_message这个Vector里元素的数量来断言。

此时的代码逻辑有问题,但是运行会报错:

error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
  --> src/lib.rs:58:13
   |
58 |             self.sent_messages.push(String::from(message));
   |             ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
   |
help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition
   |
2  ~     fn send(&mut self, msg: &str);
3  | }
...
56 |     impl Messenger for MockMessenger {
57 ~         fn send(&mut self, message: &str) {
   |

For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error

错误在为MockMessenger实现Messenger trait时定义send方法的过程:

impl Messenger for MockMessenger {
    fn send(&self, message: &str) {
        self.sent_messages.push(String::from(message));
    }
}

无法修改MockMessenger来跟踪消息,因为send方法的函数签名参数是对self不可变引用。我们也无法使用&mut self来代替,因为这样send的签名将与Messenger trait定义中的签名&self不匹配。

针对这种需要内部可变性的情况,就可以使用RefCell<T>,只需要把MockMessengersent_messages字段用RefCell<T>再包装一下即可:

struct MockMessenger {
    sent_messages: RefCell<Vec<String>>,
}

由于RefCell<T>不在预导入模块中,所以在使用它之前得先把它引入当前作用域

use std::cell::RefCell;

这样改了之后使用了sent_messages字段的代码都需要使用RefCell<T>再包装一下:

impl MockMessenger {
    fn new() -> MockMessenger {
        MockMessenger {
            sent_messages: RefCell::new(vec![]),
        }
    }
}

RefCell到底是怎么用的呢?其实就是用RefCell创建的数据,可以用borrow_mut方法来修改,对实参调用borrow_mut方法即可获得一个可变引用,所以为MockMessenger实现Messenger trait时定义send方法就可以使用borrow_mut

impl Messenger for MockMessenger {
    fn send(&self, message: &str) {
        self.sent_messages.borrow_mut().push(String::from(message));
    }
}

这样即使send的参数是不可变引用,在函数体里也可以通过borrow_mut来修改其值。

最后把测试函数的断言部分改一下:

fn it_sends_an_over_75_percent_warning_message() {
    let mock_messenger = MockMessenger::new();
    let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

    limit_tracker.set_value(80);

    assert_eq!(mock_messenger.sent_messages..borrow().len(), 1);
}

mock_messenger使用borrow函数即可获取对该变量的不可变引用用于断言。

这时候运行就没有问题了,整体代码如下:

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    struct MockMessenger {
        sent_messages: RefCell<Vec<String>>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: RefCell::new(vec![]),
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.borrow_mut().push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
	    let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

	    limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
    }
}

15.6.5. 使用RefCell<T>在运行时记录借用信息

实际上,上文所使用的borrow_mutborrow方法相当于提供给用户的两个安全接口:

  • borrow:返回智能指针Ref<T>,它实现了Deref trait
  • borrow_mut:返回智能指针RefMut<T>,实现了Deref trait

RefCell<T>会记录当前存在多少活跃的Ref<T>RefMut<T>

  • 每次调用borrow:不可变借用计数加1。
    任何一个Ref<T>的值离开作用域被释放:不可变借用计数减1
  • 每次调用borrow_mut:可变借用计数加1
    任何一个RefMut<T>的值离开作用域被释放:可变借用计数减1

与编译时借用规则(详见 4.4. 引用与借用)一样, RefCell<T>允许我们在任何时间点拥有许多不可变借用或一个可变借用。

如果我们尝试违反这些规则, RefCell<T>的实现将在运行时出现恐慌(因为RefCell<T>在运行时才会进行借用规则检查)。出现恐慌 already borrowed: BorrowMutError 就是RefCell<T>在运行时处理违反借用规则的方式。

15.6.6. 将Rc<T>RefCell<T>结合使用的例子

Rc<T>允许某些数据被多个所有者持有,但它只提供对该数据的不可变访问。如果您有一个包含RefCell<T>Rc<T> ,你可以获得一个可以拥有多个所有者并且可变的值。

下面看一个将Rc<T>RefCell<T>结合使用来实现多重数据所有权的可变数据:

#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let value = Rc::new(RefCell::new(5));

    let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

    let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));

    *value.borrow_mut() += 10;

    println!("a after = {a:?}");
    println!("b after = {b:?}");
    println!("c after = {c:?}");
}

还记得上一篇文章的Cons列表示例吗?其中我们使用了Rc<T>允许多个列表共享另一个列表的所有权。因为Rc<T>仅保存不可变值,一旦创建了列表中的任何值,我们就无法更改它们。通过这篇文章的内容,让我们添加RefCell<T>以获得更改列表中的值的能力:

  • 首先在生命枚举类型List时把Cons关联的i32类型用RefCell<>包裹,由于Rust编译器无法确定RefCell<T>大小,得用Rc<>包裹在外,其余保持不变
  • 记得引入RcRefCell到当前作用域
  • 下面通过Rc::new()RefCell::new()来创建实例,a通过Rc::clone()来共享value的值,bc通过Rc::clone()来共享a的值(前提是aRc<>包裹)。
  • 最后通过RefCell<T>上的borrow_mut获得value的可变引用,其类型时&i32,然后通过解引用符号*变为i32来进行加10的操作。

输出:

a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))

跟预期一样,没有问题。

15.6.7. 其他可以实现内部可变性的类型

  • Cell<T>:通过复制来访问数据
  • Mutex<T>:用于实现跨线程情况下的内部可变性模5

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/960653.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

lightweight-charts-python 包 更新 lightweight-charts.js 的方法

lightweight-charts-python 是 lightweight-charts.js 的 python 包装&#xff0c;非常好用 lightweight-charts 更新比较频繁&#xff0c;导致 lightweight-charts-python 内置的 lightweight-charts 经常不是最新的。 新的 lightweight-charts 通常可以获得性能改进和bug修复…

2025多目标优化创新路径汇总

多目标优化是当下非常热门且有前景的方向&#xff01;作为AI领域的核心技术之一&#xff0c;其专注于解决多个相互冲突的目标的协同优化问题&#xff0c;核心理念是寻找一组“不完美但均衡”的“帕累托最优解”。在实际中&#xff0c;几乎处处都有它的身影。 但随着需求场景的…

Python从0到100(八十五):神经网络-使用迁移学习完成猫狗分类

前言: 零基础学Python:Python从0到100最新最全教程。 想做这件事情很久了,这次我更新了自己所写过的所有博客,汇集成了Python从0到100,共一百节课,帮助大家一个月时间里从零基础到学习Python基础语法、Python爬虫、Web开发、 计算机视觉、机器学习、神经网络以及人工智能…

第4章 神经网络【1】——损失函数

4.1.从数据中学习 实际的神经网络中&#xff0c;参数的数量成千上万&#xff0c;因此&#xff0c;需要由数据自动决定权重参数的值。 4.1.1.数据驱动 数据是机器学习的核心。 我们的目标是要提取出特征量&#xff0c;特征量指的是从输入数据/图像中提取出的本质的数 …

深度学习|表示学习|卷积神经网络|详细推导每一层的维度变化|14

如是我闻&#xff1a; 一个经典的卷积神经网络&#xff08;CNN&#xff09;架构&#xff0c;呈现的是输入图像通过多个卷积层、池化层以及全连接层&#xff0c;最终输出分类结果的过程。整个过程的核心是理解输入特征图的尺寸如何在每一层发生变化&#xff0c;我们可以通过卷积…

java基础——专题一 《面向对象之前需要掌握的知识》

目录 Δ前言 一、拾枝杂谈 1.Java是什么&#xff1f; 2.计组前瞻&#xff1a; 3.JDK&#xff0c;JRE&#xff0c;JVM&#xff1f; 二、环境搭建 1.JDK安装和配置&#xff1a; 1.1 人话 1.2 JDK的配置 1.3 如何切换JDK的版本&#xff1f; 2.DOS的简单使用&#xff1a; 2.1 介…

强化学习数学原理(三)——值迭代

一、值迭代过程 上面是贝尔曼最优公式&#xff0c;之前我们说过&#xff0c;f(v)v&#xff0c;贝尔曼公式是满足contraction mapping theorem的&#xff0c;能够求解除它最优的策略和最优的state value&#xff0c;我们需要通过一个最优v*&#xff0c;这个v*来计算状态pi*&…

2025蓝桥杯JAVA编程题练习Day1

1.刑侦科推理试题 题目描述 有以下10道单选题&#xff0c;编程求这10道题的答案。 这道题的答案是&#xff1a; A. A B. B C. C D. D 第5题的答案是&#xff1a; A. C B. D C. A D. B 以下选项中哪一题的答案与其他三项不同&#xff1a; A. 第3题 B. 第6题 C. 第2题 D.…

内存泄漏的通用排查方法

本文聊一聊如何系统性地分析查找内存泄漏的具体方法&#xff0c;但不会具体到哪种语言和具体业务代码逻辑中&#xff0c;而是会从 Linux 系统上通用的一些分析方法来入手。这样&#xff0c;不论你使用什么开发语言&#xff0c;不论你在开发什么&#xff0c;它总能给你提供一些帮…

定时器按键tim_key模版

低优先级放在高优先级内势必是程序卡死 把高优先级放到低优先级内&#xff0c;会使程序卡死 可修改 Debuger调试方法 Pwm rcc #include "my_main.h" uint8_t led_sta0x10; char text[30]; void LED_Disp(uint8_t dsLED) {HAL_GPIO_WritePin(GPIOC,GPIO_PIN_All,GPI…

MacOS 如何解决无法打开 ‘xxx’,因为 Apple 无法检查其是否包含恶意软件

背景 在安装软件时&#xff0c;遇到“无法打开 ‘xxx’&#xff0c;因为 Apple 无法检查其是否包含恶意软件” 的提示&#xff0c;许多用户可能会感到困惑&#xff0c;不知道该如何处理。遇到这个问题时&#xff0c;按以下步骤操作即可解决。 首先&#xff0c;这个警告提示的出…

数据结构与算法学习笔记----求组合数

数据结构与算法学习笔记----求组合数 author: 明月清了个风 first publish time: 2025.1.27 ps⭐️一组求组合数的模版题&#xff0c;因为数据范围的不同要用不同的方法进行求解&#xff0c;涉及了很多之前的东西快速幂&#xff0c;逆元&#xff0c;质数&#xff0c;高精度等…

柔性数组与c/c++程序中内存区域的划分

1.柔性数组 1.1柔性数组的定义 柔性数组是指在结构体中定义的&#xff0c;其大小在编译时未确定&#xff0c;而在运行时动态分配的数组。这种数组允许结构体的大小根据需要动态变化。语法如下&#xff1a; struct D {int a;int arry1[0]; };struct F {int a;int arry2[]; };…

Vivado生成X1或X4位宽mcs文件并固化到flash

1.生成mcs文件 01.在vivado里的菜单栏选择"tools"工具栏 02.在"tools"里选择"生成内存配置文件" 03.配置参数 按照FPGA板上的flash型号进行选型&#xff0c;相关配置步骤可参考下图。 注意&#xff1a;Flash数据传输位宽如果需要选择X4位宽&am…

云原生:构建现代化应用的基石

一、什么是云原生&#xff1f; 云原生是一种构建和运行应用程序的方法&#xff0c;旨在充分利用云计算的分布式系统优势&#xff0c;例如弹性伸缩、微服务架构、容器化技术等。云原生应用程序从设计之初就考虑到了云环境的特点&#xff0c;能够更好地适应云平台的动态变化&…

力扣动态规划-12【算法学习day.106】

前言 ###我做这类文章一个重要的目的还是给正在学习的大家提供方向&#xff08;例如想要掌握基础用法&#xff0c;该刷哪些题&#xff1f;建议灵神的题单和代码随想录&#xff09;和记录自己的学习过程&#xff0c;我的解析也不会做的非常详细&#xff0c;只会提供思路和一些关…

day7手机拍照装备

对焦对不上&#xff1a;1、光太暗&#xff1b;2、离太近&#xff1b;3、颜色太单一没有区分点 滤镜可以后期P 渐变灰滤镜&#xff1a;均衡色彩&#xff0c;暗的地方亮一些&#xff0c;亮的地方暗一些 中灰滤镜&#xff1a;减少光差 手机支架&#xff1a;最基本70cm即可 手…

解锁微服务:五大进阶业务场景深度剖析

目录 医疗行业&#xff1a;智能诊疗的加速引擎 电商领域&#xff1a;数据依赖的破局之道 金融行业&#xff1a;运维可观测性的提升之路 物流行业&#xff1a;智慧物流的创新架构 综合业务&#xff1a;服务依赖的优化策略 医疗行业&#xff1a;智能诊疗的加速引擎 在医疗行业迈…

LosslessScaling-学习版[steam价值30元的游戏无损放大/补帧工具]

LosslessScaling 链接&#xff1a;https://pan.xunlei.com/s/VOHc-yZBgwBOoqtdZAv114ZTA1?pwdxiih# 解压后运行"A-绿化-解压后运行我.cmd"

51单片机开发:点阵屏显示数字

实验目标&#xff1a;在8x8的点阵屏上显示数字0。 点阵屏的原理图如下图所示&#xff0c;点阵屏的列接在P0端口&#xff0c;行接在74HC595扩展的DP端口上。 扩展口的使用详见&#xff1a;51单片机开发&#xff1a;IO扩展(串转并)实验-CSDN博客 要让点阵屏显示数字&#xff0…