2024年1月

前言

生成分布式唯一ID的方式有很多种如常见的有UUID、Snowflake(雪花算法)、数据库自增ID、Redis等等,今天我们来讲讲.NET集成IdGenerator生成分布式全局唯一ID。

分布式ID是什么?

分布式ID是一种在分布式系统中生成唯一标识符的方法,用于解决多个节点之间标识符重复或性能问题。

分布式ID需要满足的条件

  • 全局唯一性:分布式ID在整个分布式系统中必须是唯一的,不同节点生成的ID不能重复。
  • 高性能:生成分布式ID的算法或机制应该具有高效的性能,能够在大规模的分布式环境下迅速生成ID。
  • 高可用 :生成分布式ID的服务要保证可用性无限接近于100%。
  • 方便易用 :拿来即用,使用方便,快速接入。
  • 压缩性:分布式ID应该尽可能地使用较少的存储空间,减少对网络传输和存储的负载。

IdGenerator介绍

IdGenerator是一个全面的分布式主键ID生成器,使用的是优化的雪花算法(SnowFlake)雪花漂移算法,在缩短ID长度的同时,具备极高瞬时并发处理能力(50W/0.1s)。

  • 这是优化的雪花算法(雪花漂移),它生成的ID更短、速度更快。
  • 支持 k8s 等容器环境自动扩容(自动注册 WorkerId),可在单机或分布式环境生成数字型唯一ID。
  • 原生支持 C#/Java/Go/Rust/C/SQL/Node.js/PHP(C扩展) 等语言,并提供Python、PB多线程安全调用动态库(FFI)。
  • 兼容所有雪花算法(号段模式或经典模式,大厂或小厂),将来你可做任意的升级切换。(一般无须升级,但理论上支持)。

C# IdGenerator支持环境

.NET Standard 2.0+ (支持:.net framework4.6.1+/.net core 2.0+/.net5/.net6+/.net7+/.net8/mono5.4+/Unity 2018+/Xamarin)。

性能数据

(参数:10位自增序列,1000次漂移最大值).

连续请求量 5K 5W 50W
传统雪花算法 0.0045s 0.053s 0.556s
雪花漂移算法 0.0015s 0.012s 0.113s

wmproxy

wmproxy
已用
Rust
实现
http/https
代理,
socks5
代理, 反向代理, 静态文件服务器,四层TCP/UDP转发,七层负载均衡,内网穿透,后续将实现
websocket
代理等,会将实现过程分享出来,感兴趣的可以一起造个轮子

项目地址

国内: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

设计目标

设置启动命令行参数方便在没有配置的情况下启动。

设计事项

当你在Linux终端上输入git --help时,你会看到这样的打印:

shell> git --help
usage: git [--version] [--help] [-C <path>] [-c <name>=<value>]
           [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
           [-p | --paginate | --no-pager] [--no-replace-objects] [--bare]
           [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
           <command> [<args>]

These are common Git commands used in various situations:

start a working area (see also: git help tutorial)
   clone      Clone a repository into a new directory
   init       Create an empty Git repository or reinitialize an existing one
...... <omitted>

Git程序支持一长串参数。我们以它为例来讨论程序的参数应该是什么样的。

为了给用户给予一个良好的命令行界面,程序的参数处理部分应该支持以下功能:

  • 接受参数的选项
  • 不带任何参数的标志/开关
  • 长名称(以双连字符开头,如--paginate)和短名称(以单连字符开头,如-p)
  • 友好的帮助信息--help
  • 子命令(如git add)

通过命令行我们可以执行程序中的非常多的功能,也就是命令行让程序可以在无需GUI的情况下拥有无限丰富的功能。

RUST中的命令行解析

在这里以bpaf来举例,也是本项目中选择的命令行解析库,相对来说他支持宏的同时也支持比较丰富的程序中的组合,也拥有较多的demo可以参考如何使用:

use bpaf::{short, Bpaf, Parser};
use std::path::PathBuf;

#[derive(Debug, Clone, Bpaf)]
#[bpaf(options, version)]
#[allow(dead_code)]
struct Opts {
    /// 是否开始调试模式
    #[bpaf(short, long)]
    debug: bool,
    /// 这是一个注释,将被忽略
    #[bpaf(external(verbose))]
    verbose: usize,
    /// 设置速度, 拥有默认速度
    #[bpaf(argument("SPEED"), fallback(42.0), display_fallback)]
    speed: f64,
    /// 输出目录
    output: PathBuf,

    /// 将检测必须为正数
    #[bpaf(guard(positive, "must be positive"), fallback(1))]
    nb_cars: u32,
    files_to_process: Vec<PathBuf>,
}

fn verbose() -> impl Parser<usize> {
    // number of occurrences of the v/verbose flag capped at 3
    short('v')
        .long("verbose")
        .help("启动verbose模式\n根据输入的v的个数来判定等级\n可以 -v -v -v 或者 -vvv")
        .req_flag(())
        .many()
        .map(|xs| xs.len())
        .guard(|&x| x <= 3, "最多仅能输入三个v")
}

fn positive(input: &u32) -> bool {
    *input > 1
}

fn main() {
    println!("{:#?}", opts().run());
}

通过启动打印--help信息可知
cargo run --example arg -- --help

PS D:\my\wmproxy> cargo run --example arg -- --help
   Compiling wmproxy v0.2.5 (D:\my\wmproxy)
    Finished dev [unoptimized + debuginfo] target(s) in 1.12s
     Running `target\debug\examples\arg.exe --help`
Usage: arg.exe [-d] [-v]... [--speed=SPEED] --output=ARG [--nb-cars=ARG] [--files-to-process=ARG]...

Available options:
    -d, --debug        是否开始调试模式
    -v, --verbose      启动verbose模式 根据输入的v的个数来判定等级 可以 -v -v -v 或者 -vvv        
        --speed=SPEED  设置速度, 拥有默认速度
                       [default: 42]
        --output=ARG   输出目录
        --nb-cars=ARG  将检测必须为正数
        --files-to-process=ARG
    -h, --help         Prints help information
    -V, --version      Prints version information

我们可以通过简单的配置来拥有较完整的命令行的模式的支持,包括默认参数
fallback
,及完整的help信息。

rust中还有其它较为知名的解析库,图片来源于网上,可根据项目的需要进行相应的选择。

命令行参数

该项目选择用的是子命令的模式

  • wmproxy run
    启动命令
  • wmproxy start
    启动命令, 但在后台运行
  • wmproxy stop
    关闭命令
  • wmproxy reload
    进行重载配置
  • wmproxy check
    检查配置是否正确
  • wmproxy proxy
    代理及内网穿透相关功能
  • wmproxy file-server
    启动文件服务器
  • wmproxy reverse-proxy
    启动负载均衡服务器
  • wmproxy ws-proxy
    Websocket协议转发相关
  • wmproxy version
    打印当前版本号

wmproxy run

通过命令
wmproxy run --help
可以得到

Usage: wmproxy.exe run -c=ARG [--pidfile=ARG] [--control=ARG] [--disable-stdout] [--disable-control]
[-v] [--default-level=ARG]

Available options:
    -c, --config=ARG         配置文件路径
        --pidfile=ARG        写入进程id文件
        --control=ARG        输入控制台的监听地址
                             [default: 127.0.0.1:8837]
        --disable-stdout     禁用默认输出
        --disable-control    禁用控制微端
    -v, --verbose            是否显示更多日志
        --default-level=ARG  设置默认等级
    -h, --help               Prints help information

通过配置文件启动程序如:

wmproxy run -c config/reverse.toml

wmproxy start

通过命令
wmproxy start --help
可以得到

Usage: wmproxy.exe start -c=ARG [--pidfile=ARG] [--control=ARG] [--disable-stdout] [--disable-control]
[-v] [--default-level=ARG]

Available options:
    -c, --config=ARG         配置文件路径
        --pidfile=ARG        写入进程id文件
        --control=ARG        输入控制台的监听地址
                             [default: 127.0.0.1:8837]
        --disable-stdout     禁用默认输出
        --disable-control    禁用控制微端
    -v, --verbose            是否显示更多日志
        --default-level=ARG  设置默认等级
    -h, --help               Prints help information

通过配置文件启动程序,我们通过观察可以得知参数与run一模一样,差别是该参数将后台启动。
示例如:

wmproxy start -c config/reverse.toml

wmproxy stop

通过命令
wmproxy stop --help
可以得到

Usage: wmproxy.exe stop [-c=ARG] [-p=ARG] [-u=ARG] [--control=ARG] [--disable-stdout] [--disable-control
] [-v] [--default-level=ARG]

Available options:
    -c, --config=ARG         配置文件路径
    -p, --pidfile=ARG        写入进程id文件
    -u, --url=ARG            控制微端地址
        --control=ARG        输入控制台的监听地址
                             [default: 127.0.0.1:8837]
        --disable-stdout     禁用默认输出
        --disable-control    禁用控制微端
    -v, --verbose            是否显示更多日志
        --default-level=ARG  设置默认等级
    -h, --help               Prints help information

此处我们通过三种方式将其关闭,一种是配置文件,一种是pid文件,一种是控制微端的url。
示例如:

wmproxy stop -c config/reverse.toml
或者
wmproxy stop -p wmproxy.pid
或者
wmproxy stop -u http://127.0.0.1:8837

wmproxy reload

通过命令
wmproxy reload --help
可以得到

Usage: wmproxy.exe reload [-c=ARG] [-u=ARG] [--control=ARG] [--disable-stdout] [--disable-control] [-v
] [--default-level=ARG]

Available options:
    -c, --config=ARG         配置文件路径
    -u, --url=ARG            控制微端地址
        --control=ARG        输入控制台的监听地址
                             [default: 127.0.0.1:8837]
        --disable-stdout     禁用默认输出
        --disable-control    禁用控制微端

此处我们通过三种方式将其重载,一种是配置文件,一种是控制微端的url。
示例如:

wmproxy reload -c config/reverse.toml
或者
wmproxy reload -u http://127.0.0.1:8837

wmproxy proxy

代理类相对参数比较复杂
通过命令
wmproxy proxy --help
可以得到

Usage: wmproxy.exe proxy [-b=ARG] [--flag=ARG] [--mode=ARG] [-S=ARG] [--user=ARG] [--pass=ARG] [--udp-bind
=ARG] [--map-http-bind=ARG] [--map-https-bind=ARG] [--map-tcp-bind=ARG] [--map-proxy-bind=ARG] [--map-cert
=ARG] [--map-key=ARG] [--center] [--ts] [--tc] [--two-way-tls] [--domain=ARG] [--cert=ARG] [--key=ARG
] [--mappings=ARG]... [--control=ARG] [--disable-stdout] [--disable-control] [-v] [--default-level=ARG
]

代理类, 一个代理类启动一种类型的代理
    -b, --bind-addr=ARG       代理绑定端口地址
                              [default: 127.0.0.1:8090]
        --flag=ARG            代理种类, 如http https socks5
        --mode=ARG            启动程序类型
    -S, --server=ARG          连接代理服务端地址
        --user=ARG            用于socks验证及中心服务器验证
        --pass=ARG            用于socks验证及中心服务器验证
        --udp-bind=ARG        udp的绑定地址
        --map-http-bind=ARG   内网http的映射地址
        --map-https-bind=ARG  内网https的映射地址
        --map-tcp-bind=ARG    内网tcp的映射地址
        --map-proxy-bind=ARG  内网代理的映射地址
        --map-cert=ARG        内网映射的证书cert
        --map-key=ARG         内网映射的证书key
        --center              是否启用协议转发
        --ts                  连接服务端是否启用tls
        --tc                  接收客户端是否启用tls
        --two-way-tls         双向认证是否启用
        --domain=ARG          tls证书所用的域名
        --cert=ARG            公开的证书公钥文件
        --key=ARG             隐私的证书私钥文件
        --mappings=ARG

Available options:
        --control=ARG         输入控制台的监听地址
                              [default: 127.0.0.1:8837]
        --disable-stdout      禁用默认输出
        --disable-control     禁用控制微端
    -v, --verbose             是否显示更多日志
        --default-level=ARG   设置默认等级
    -h, --help                Prints help information

如果仅仅启动代理功能可以

wmproxy proxy -b="127.0.0.1:8090"
或者
wmproxy proxy -b "127.0.0.1:8090"

即可启动8090开始的http/https/socks5代理

wmproxy file-server

启动文件服务
通过命令
wmproxy file-server --help
可以得到

Usage: wmproxy.exe file-server [-r=ARG] [-l=ARG] [-d=ARG] [-b] [--access-log=ARG] [--control=ARG] [--disable-stdout
] [--disable-control] [-v] [--default-level=ARG]

Available options:
    -r, --root=ARG           静态文件根目录路径
    -l, --listen=ARG         监听地址
                             [default: 127.0.0.1:80]
    -d, --domain=ARG         域名地址
    -b, --browse             是否支持目录
        --access-log=ARG     访问日志放的位置如"logs/access.log trace"
        --control=ARG        输入控制台的监听地址
                             [default: 127.0.0.1:8837]
        --disable-stdout     禁用默认输出
        --disable-control    禁用控制微端
    -v, --verbose            是否显示更多日志
        --default-level=ARG  设置默认等级
    -h, --help               Prints help information

如果仅仅启动代理功能可以

wmproxy file-server
或者
wmproxy file-server -l :8080 -r "d:/my/wmproxy"

即可启动文件服务器对其服务。

wmproxy reverse-proxy

启动负载均衡服务
通过命令
wmproxy reverse-proxy --help
可以得到

Usage: wmproxy.exe reverse-proxy [-f=ARG] -t=ARG [-H=ARG]... [--access-log=ARG] [--tls] [--control=ARG
] [--disable-stdout] [--disable-control] [-v] [--default-level=ARG]

Available options:
    -f, --from=ARG           负载均衡来源地址
                             [default: 127.0.0.1:80]
    -t, --to=ARG             负载均衡映射地址
    -H, --header=ARG         头部信息修改如 "proxy x-forward-for {client_ip}"
        --access-log=ARG     访问日志放的位置如"logs/access.log trace"
        --tls                是否映射到https上
        --control=ARG        输入控制台的监听地址
                             [default: 127.0.0.1:8837]
        --disable-stdout     禁用默认输出
        --disable-control    禁用控制微端
    -v, --verbose            是否显示更多日志
        --default-level=ARG  设置默认等级
    -h, --help               Prints help information

需要配置从源目标到目标的信息,可以配置header等相应的信息。

wmproxy reverse-proxy
或者
wmproxy reverse-proxy -t :8080

即可启动负载均衡服务对其服务。

wmproxy ws-proxy

启动websocket协议转发服务
通过命令
wmproxy ws-proxy --help
可以得到

Usage: wmproxy.exe ws-proxy [-f=ARG] -t=ARG [--access-log=ARG] --mode=<ws2tcp,tcp2ws,tcp2wss> [--ws]
[--control=ARG] [--disable-stdout] [--disable-control] [-v] [--default-level=ARG]

Available options:
    -f, --from=ARG           负载均衡来源地址
                             [default: 127.0.0.1:80]
    -t, --to=ARG             负载均衡映射地址
        --access-log=ARG     访问日志放的位置如"logs/access.log trace"
        --mode=<ws2tcp,tcp2ws,tcp2wss>  当前代理的模式
        --domain=ARG         当前域名
        --ws                 是否支持websocket
        --control=ARG        输入控制台的监听地址
                             [default: 127.0.0.1:8837]

模式中

  • ws2tcp 指websocket的流量转成tcp
  • tcp2ws 指tcp的流量转成websocket
  • tcp2wss 指tcp的流量转成websocket tls流量
# 将流量通过ws转成tcp
wmproxy ws-proxy -t :8082 --mode ws2tcp
或者
# 将流量通过tcp转成ws
wmproxy ws-proxy -t :8099 --mode tcp2ws

即可启动websocket的流量。

wmproxy version

打印当前软件的版本号

小结

好的命令行可以执行更强大的功能,也能让人更好的理解你的程序,这需要有一个好的帮助系统,也需要有一个强大的解析能力。此章讲述了命令行的执行功能,希望可以帮助到你的使用。

点击
[关注]

[在看]

[点赞]
是对作者最大的支持

最近团队有小伙伴问在单元测试中如何Mock HttpContext. 这是一个好问题,整理了一个实现方案分享给大家。

在C#中,尤其是在单元测试场景下,模拟(Mocking)HttpContext 是一种常见的做法。这允许我们在没有实际HTTP请求的情况下测试与HTTP上下文相关的代码。

为了模拟HttpContext,我们通常会使用像Moq这样的库,它是.NET中一个流行的模拟框架。
以下是一个简单的示例,展示了如何使用Moq来模拟一个HttpContext:
首先,你需要安装Moq库。如果你使用的是.NET Core或.NET 5/6/7/8等较新版本,你可以通过NuGet包管理器来安装它:

dotnet add package Moq

或者,在Visual Studio中,你可以通过NuGet包管理器UI来搜索并安装Moq。

安装完成后,编写以下代码来创建一个模拟的HttpContext.

usingSystem.Security.Claims;usingSystem.Web;usingMoq;//创建一个模拟的HttpContext
var mockContext = new Mock<HttpContextBase>();//模拟HttpRequest
var mockRequest = new Mock<HttpRequestBase>();
mockRequest.Setup(r
=> r.ApplicationPath).Returns("/");
mockRequest.Setup(r
=> r.ServerVariables).Returns(newSystem.Collections.Specialized.NameValueCollection());
mockContext.Setup(c
=>c.Request).Returns(mockRequest.Object);//模拟HttpResponse var mockResponse = new Mock<HttpResponseBase>();
mockResponse.Setup(r
=> r.Cache).Returns(newHttpCachePolicyBase());
mockContext.Setup(c
=>c.Response).Returns(mockResponse.Object);//模拟HttpSessionState var mockSession = new Mock<HttpSessionStateBase>();
mockSession.Setup(s
=> s["MySessionItem"]).Returns("SessionValue");
mockContext.Setup(c
=>c.Session).Returns(mockSession.Object);//模拟HttpServerUtility var mockServer = new Mock<HttpServerUtilityBase>();
mockContext.Setup(c
=>c.Server).Returns(mockServer.Object);//模拟HttpUser var mockUser = new Mock<IPrincipal>();var mockIdentity = new Mock<IIdentity>();
mockIdentity.Setup(i
=> i.Name).Returns("User");
mockIdentity.Setup(i
=> i.IsAuthenticated).Returns(true);
mockUser.Setup(u
=>u.Identity).Returns(mockIdentity.Object);
mockContext.Setup(c
=>c.User).Returns(mockUser.Object);//现在你可以使用 mockContext.Object 来作为你的HttpContextBase对象了 var httpContext =mockContext.Object;//使用示例: var userName = httpContext.User.Identity.Name; //返回 "User" var isAuthenticated = httpContext.User.Identity.IsAuthenticated; //返回 true var mySessionItem = httpContext.Session["MySessionItem"]; //返回 "SessionValue"

更进一步,我们可以把这个HttpContext的mock封装成一个服务,团队其他的小伙伴也可以一起使用。

so,为了使其更加模块化和可重用,我们可以将这些模拟的创建过程封装到一个方法中,或者创建一个专门的服务类来提供
HttpContextBase
的模拟实例。

usingSystem.Collections.Specialized;usingSystem.Security.Principal;usingSystem.Web;usingMoq;public static classMockHttpContextService
{
public staticHttpContextBase CreateMockHttpContext(string userName = "User",bool isAuthenticated = true,string sessionKey = "MySessionItem",object sessionValue = null,string applicationPath = "/",
NameValueCollection serverVariables
= null)
{
var mockContext = new Mock<HttpContextBase>();//模拟HttpRequest var mockRequest = new Mock<HttpRequestBase>();
mockRequest.Setup(r
=>r.ApplicationPath).Returns(applicationPath);
mockRequest.Setup(r
=> r.ServerVariables).Returns(serverVariables ?? newNameValueCollection());
mockContext.Setup(c
=>c.Request).Returns(mockRequest.Object);//模拟HttpResponse var mockResponse = new Mock<HttpResponseBase>();
mockResponse.Setup(r
=> r.Cache).Returns(newHttpCachePolicyBase());
mockContext.Setup(c
=>c.Response).Returns(mockResponse.Object);//模拟HttpSessionState var mockSession = new Mock<HttpSessionStateBase>();if (sessionKey != null)
{
mockSession.Setup(s
=>s[sessionKey]).Returns(sessionValue);
}
mockContext.Setup(c
=>c.Session).Returns(mockSession.Object);//模拟HttpServerUtility var mockServer = new Mock<HttpServerUtilityBase>();
mockContext.Setup(c
=>c.Server).Returns(mockServer.Object);//模拟HttpUser var mockUser = new Mock<IPrincipal>();var mockIdentity = new Mock<IIdentity>();
mockIdentity.Setup(i
=>i.Name).Returns(userName);
mockIdentity.Setup(i
=>i.IsAuthenticated).Returns(isAuthenticated);
mockUser.Setup(u
=>u.Identity).Returns(mockIdentity.Object);
mockContext.Setup(c
=>c.User).Returns(mockUser.Object);returnmockContext.Object;
}
}

创建模拟
HttpContext
对象时传入自定义参数:

var httpContext =MockHttpContextService.CreateMockHttpContext(
userName:
"CustomUser",
isAuthenticated:
false,
sessionKey:
"CustomSessionItem",
sessionValue:
"CustomSessionValue",
applicationPath:
"/MyApp",
serverVariables:
new NameValueCollection { { "SERVER_NAME", "localhost"} }
);
//使用示例: var userName = httpContext.User.Identity.Name; //返回 "CustomUser" var isAuthenticated = httpContext.User.Identity.IsAuthenticated; //返回 false var mySessionItem = httpContext.Session["CustomSessionItem"]; //返回 "CustomSessionValue" var appPath = httpContext.Request.ApplicationPath; //返回 "/MyApp" var serverName = httpContext.Request.ServerVariables["SERVER_NAME"]; //返回 "localhost"

以上是单元测试中如何Mock HttpContext的分享,希望能帮助到大家。

周国庆

2024/1/24

liwen01  2024.01.01

前言

从系统角度来看,文件系统是对文件存储器空间进行组织、分配、负责文件存储并对存入的文件进行保护和检索的一个系统。

它的目的是使用户和应用程序能够方便地管理存储器上的文件和目录,比如常见的对文件的增、删、改、查。

每当我们操作一个文件的时候,实际都会涉及到存储器、管理系统、被管理文件,还有就是实施文件管理所需的数据结构。

存储器,我们也叫存储介质,也就是电子设备用来存储数据信息的器件。

(一)存储介质介绍

存储介质,也就是用来存储信息的介质,从远古的石壁刻画,到古代的笔墨纸砚记录,再到电气时代的磁存储、光存储、半导体芯片存储。

光盘存储已近逐渐地退出大众的视野,我们这里主要介绍磁存储和半导体存储。磁存储的代表就是机械硬盘,半导体存储已近被非常广泛地应用了,比如我们常用的U盘、SD卡、TF卡、固态硬盘等。

  • 机械硬盘因为它容量大、价格低,在个人电脑中广泛使用,
  • 固态存储速度快,功耗低,无噪音,同等容量的价格也比较的平民,在各种便携式存储设备中广泛使用。

(二)固态存储

固态存储,也就是不依赖机械传动,直接使用半导体芯片为介质的一种存储。大的分类可以分为两种: RAM和ROM。

Flash其实也算是ROM的一种变种,现在广泛使用的 ROM 大部分都是基于Flash设计的。大部分固态硬盘的存储介质也是Flash。

(1)RAM与ROM

RAM(随机访问存储器)
:

是一种临时存储器,用于保存正在运行时所需的数据和程序,可读可写,允许在需要时快速读取或写入数据,但它也是易失性存储器,设备断电时里面的数据会丢失。

ROM(只读存储器)
:

用于存储固定的、不经常变化的数据和程序,它是只读的,通常在制造时被写入,并且其内容在正常操作期间不可更改。但它存在一些变种,如可擦写可编程只读存储器(EPROM)和闪存(Flash),它们允许有限次的可编程操作。

Flash存储器

它是一种非易失性存储技术,广泛用于各种电子设备、USB、固态硬盘等产品。它分为NOR Flash 和NAND Flash 两种

(2) NOR Flash

它采用 NOR(不或非)门结构,与NAND Flash相比,NOR Flash具有一些特定的特性和优势:

  • 读取速度快
  • 随机访问能力
  • 适用于代码存储
  • 写入擦除速度较慢
  • 低功耗

NOR门结构

NOR Flash,
NOR就是“非或”的意思

NOR门是由两个输入端(A和B)和一个输出端组成的。其输出取决于输入信号的状态,具体逻辑运算规则如下:

  • 输出为高电平(1)的条件:只有当A和B都为低电平(0)时,NOR门的输出才为高电平。
  • 输出为低电平(0)的条件:如果A或B中至少有一个为高电平,NOR门的输出就会是低电平。

NOR门的逻辑运算符号通常用符号“+”表示,其输出(Y)可以表示为:Y = A + B(其中,“+”表示逻辑或运算)。

NOR门的
逻辑符号
如下所示:

    -----
A --|     |
    | NOR |---- Y
B --|     |
    -----

(3) NAND Flash

NAND Flash采用NAND门结构,与NOR Flash相比,它具有一些独特的特点和优势。

  • 高密度存储
  • 相对低成本
  • 擦写熟读快
  • 适用于大容量数据存储
  • 顺序访问

NAND门结构:

NAND门由两个输入端(A和B)和一个输出端组成。其输出取决于输入信号的状态,具体逻辑运算规则如下:

  • 输出为高电平(1)的条件:当A和B都为高电平时,NAND门的输出为低电平(0)。

  • 输出为低电平(0)的条件:只有当A和B中至少有一个为低电平时,NAND门的输出才为高电平。

NAND门的逻辑运算符号通常用“⊼”(或称“AND非”)表示
,其输出(Y)可以表示为:Y = A ⊼ B(其中,“⊼”表示逻辑AND非运算)。

NAND门的逻辑符号如下所示:

    -----
A --|     |
    | NAND|---- Y
B --|     |
    -----

(4)基于Flash的应用

嵌入式设备中的应用

在嵌入式设备,一般是直接使用Flash存储器来存储程序和代码,按总线类型,可以分为串行和并行总线接口,在嵌入式设备中使用的比较多的是SPI
串行总线

比如常用来存储程序的SPI NOR Flash:
PY25Q128HA、MX25L51245G、FM25Q128A、EN25QH128A、BY25Q128AS

它的特点是容量较小,可以随机访问。

组合应用

我们日常生活中使用到的固态存储器,比如U盘、SD卡、TF卡、固态硬盘等,它里面除了Flash存储芯片外,还有主控芯片、DDR内存、接口、PCB电路板。

  • Flash存储芯片
    :是主要的存储介质,通常采用NAND型闪存。
  • 主控芯片
    :是固态硬盘的核心控制单元,负责管理数据的读写、错误校正、垃圾回收和性能优化等任务。
  • DDR(双数据速率缓存):
    用于缓存数据和提高读写速度
  • 接口
    :决定了存储设备与主板之间的数据传输速度和通信方式
  • PCB电路板
    :提供了这些组件之间的电气连接

下面是一个固态硬盘的内部器件,其它的固态存储设备也类似。

我们日常使用到的固态存储设备,比如TF卡、SD卡、U盘等,一般内部使用的都是NAND flash,它的优点是:存储密度高、改写速度快,同等容量下价格相对比较便宜。

(三)固态存储架构体系

(1)NAND flash 特性

我们在上面看到一块固态硬盘内部包括了主控芯片、DDR内存、接口、PCB电路板,为啥需要这么复杂?为什么不能直接操作Flash芯片呢?主要原因是跟NAND flash 的特性有关系。

NAND flash 有些独特的物理组成和使用特性:

(a)写入的最小单元是页

NAND Flash 存储器的最小可编程单元是页。每次写入操作都需要覆盖整个页,而不能仅修改其中的一小部分。这使得写入操作的最小单元限定在一页的大小,即使只有一页中的一小部分数据需要更新,整个页也需要被写入。

(b)擦除的最小单元是块

NAND Flash 存储器的最小可擦除单元是块。在擦除之前,需要将整个块的数据读取出来,修改后的数据写回。由于擦除操作是比较慢且磨损较大的操作,为了降低擦除的频率,设计选择以页为单位进行写入,而不是以块为单位。

典型的页大小为2KB、4KB或8KB,常见的块大小为64页或128页。

(c)擦写次数有限制

每个块都有一个有限的擦写寿命,即可以执行的擦写次数是有限的。频繁地在原地更新数据可能导致某些块更快达到擦写寿命上限,进而影响整个NAND Flash的寿命。因此,设计者更倾向于采用写入新页并擦除旧块的策略,以减少擦写次数的集中发生。

(d)随机位翻转

NAND Flash芯片中会因为自然放电、电场噪声、辐射等原因出现随机位翻转的现象,导致存储的数据位发生不可预测的变化。

(2)flash FTL

因为 nand flash 有擦写次数的限制、有随机位翻转的问题、还有写入和擦除大小不一致的限制,现有针对机械磁盘设计的文件系统是不能直接应用在Flash上的,需要借助一个特殊的软件层——闪存转换层(FTL),来屏蔽闪存的底层差异,使上层系统可以透明地使用闪存。

相应的,固态存储的软件架构也要复杂很多。

(3)地址映射

FTL 中的地址映射确保上层文件系统可以使用逻辑块地址,而不用担心 NAND Flash 存储器的底层细节。这种映射关系的管理是 FTL 在保障 NAND Flash 数据稳定性、提高寿命以及提供高效读写的关键功能之一

逻辑块地址(LBA):

这是上层文件系统使用的块地址,通常是顺序递增的逻辑块编号。应用程序或文件系统将数据按逻辑块地址组织,而不需要关心底层 NAND Flash 存储的特性。

物理块地址:

这是 NAND Flash 存储器中实际的物理块的地址。由于 NAND Flash 进行擦写时只能对整个块进行,FTL 负责将逻辑块地址映射到物理块地址,并处理擦写时的块移动操作。

映射表

FTL 维护一个映射表,记录逻辑块地址到物理块地址的映射关系。这个映射表通常存储在 Flash 存储器的特殊区域,如页或块中。

写入时的地址映射:

当应用程序向 NAND Flash 写入数据时,FTL 将逻辑块地址映射到一个可用的物理块。如果该物理块已经被写入过数据,FTL 需要擦除该物理块,将其标记为可用,并将新数据写入。这一过程需要更新映射表。

擦除时的块移动:

由于 NAND Flash 存储器的特性,每次擦写操作都会引发整个块的擦除。当需要擦除一个物理块时,FTL 需要将该块中的有效数据移动到其他块,然后标记该块为可用。这一过程同样需要更新映射表。

(4)垃圾回收

为什么会产生垃圾?

(a)不可原位更新:

当需要更新NAND Flash中的数据时,无法直接在原地进行更新。相反,新的数据必须写入一个新的空白页中,而原有的数据所在的页则被标记为无效。这导致存储器中同时存在有效和无效的数据,即产生了垃圾。

(b)擦写操作的不可细粒度

NAND Flash的最小擦除单位是一个块,而最小写入单位是一页。如果需要更新某个页面的数据,整个块都需要被擦除,然后将更新后的数据写入。由于擦写和写入的单位不同,更新操作可能会导致未更改的数据也被重新写入,形成了垃圾。

(c)擦除前数据的标记:

当需要删除数据时,NAND Flash通常会将相应的页标记为无效,而不是立即擦除。这样,数据被删除后,原来的页仍然存在,但被标记为无效状态,形成了垃圾。

(d)写入放大:

写入放大是指为了写入少量数据而触发的大量物理写入。由于不可原位更新和擦写操作的不可细粒度,写入操作可能导致更多的物理页被写入,从而增加了实际写入量,也增加了垃圾的产生。

这些因素共同作用,导致NAND Flash中垃圾的产生。为了解决这个问题,存储系统采用垃圾回收机制,定期清理无效的数据,擦除无效块,以便重新利用存储空间,并最大程度地减少擦写次数,延长NAND Flash的寿命。

垃圾回收的一般流程:

  • 标记无效页
  • 选择目标块
  • 数据迁移
  • 擦除目标块
  • 更新元数据

(5)坏块管理

为什么会产生坏块?

(a)擦写次数限制:

NAND Flash的每个块都有一个有限的擦写寿命,即可以执行的擦写次数是有限的。当某个块达到了其擦写寿命上限,该块就会被标记为坏块,不再可用。这是由于擦写操作引起的物理损耗。

(b)随机位翻转:

存储器单元可能受到环境中的辐射、电场噪声或其他干扰,导致数据位发生随机翻转。当这种情况发生在存储器中的某个块时,该块可能变成坏块,因为存储的数据无法正确读取。

(c)存储介质的不稳定性:

NAND Flash使用的存储介质(通常是浮栅)可能因为制造过程或质量问题而导致某些块不稳定。这种不稳定性可能导致块的性能下降,或者在使用中产生坏块。

(d)擦写时的错误:

在擦写操作中,由于电压不稳定、温度变化或其他因素,可能发生错误。这些错误可能导致擦写操作未能正确完成,从而使得块成为坏块。

(e)制造过程中的缺陷

制造过程中可能存在一些缺陷,导致某些块在生产阶段就不完整或有问题。这些块可能在设备制造过程中被标记为坏块。

坏块管理的一般流程

(a)坏块检测:

  • 初始化阶段:在系统初始化时,进行坏块检测。这包括检查 NAND Flash 存储器中是否存在已经标记为坏块的块。
  • 运行时检测:在系统运行时,定期或在需要时进行坏块检测。这可以通过读取存储器中的坏块表或直接尝试读取每个块的方式来实现。

(b)坏块标记:

如果检测到坏块,将其标记为坏块。这可以通过在坏块表中记录坏块的位置或使用其他元数据方式来实现。

(c)坏块替代:

一旦坏块被检测并标记,系统需要寻找可替代的块。这通常包括从备用块池中选择一个可用的块来替代坏块。备用块通常是在 NAND Flash 设备制造时预留的。

(d)更新坏块表:

如果替代块被选择,需要更新坏块表,将新块的位置标记为有效块。这确保了系统在后续操作中不会再使用已标记为坏块的块。

(e)数据迁移:

如果坏块替代发生在运行时,系统可能需要将原始块中的有效数据迁移到替代块中,以确保数据的完整性和可靠性。

(6)损耗均匀(磨损平衡)

磨损均衡是为了最大程度地平衡 NAND Flash 存储器中擦写寿命的消耗,从而提高存储器的性能、可靠性和寿命。

磨损均衡的策略一般有:

(a)块循环使用:

块循环使用是最简单的磨损均衡策略之一。它通过轮流使用存储器中的块,而不是过度依赖于特定的块。这有助于分散擦写操作,减轻存储单元的磨损。

(b)Wear Leveling(磨损均衡)算法:
Wear Leveling 算法是一种专门设计的算法,旨在在整个存储器中均匀分配擦写操作。这种算法考虑每个块的使用情况,优化写入和擦除操作的分布,以降低存储单元的不均匀磨损。常见的
Wear Leveling
算法包括静态均衡和动态均衡算法。

(c)动态坏块管理:

动态监测并管理坏块,确保它们不会集中在特定区域。当检测到新的坏块时,系统可以调整块的分布,以确保损耗均匀。

(d)块交换:

进行块交换,将频繁写入的块与很少写入的块进行交换。这有助于平衡擦写操作,减缓块的磨损。块交换可以是静态的,也可以是动态的,根据系统的需要进行调整。

(e)备用块池的合理使用:

在 NAND Flash 制造时,通常会预留一些备用块。合理使用备用块池,确保块的磨损相对均匀。

(f)周期性的擦除操作:

定期执行擦除操作,即使块中没有无效页。这有助于重置块的状态,减小块的磨损程度。

(7)为什么手机越用越卡

手机越用越卡,除了器件的老化问题之外,还有一个原因就是存储设备里面的数据接近存储卡容量,导致存储设备写数据变慢了。

为什么容量满了就会变慢了呢?这主要与写放大和垃圾回收有关系。

假设要写入一个4KB的数据,但已经没有干净空间了,只有失效的数据可以擦除,所以主控就把所有的数据读取到缓存存,然后擦除一个块,再加上这个4KB新数据写回去,这就造成了写入放大,本来是需要写4K的数据,却写入了512K的数据,也就是128倍放大。

上面只是个假设,实际有垃圾回收策略的接入,不会是128倍这么大。下面是IBM Zurich Research Laboratory做的研究,实际证明写放大与空闲净块关系极大

上图可以看出,空闲块少后,写放大可以到达3~4倍

(四)机械硬盘

尽管固态存储设备发展迅猛,但传统的机械硬盘存储还是有很大的市场占有量,因为它数据稳定,同等价格下,存储容量更大。

(1)外部结构

我们从外部看,一个机械硬盘上有:主控芯片、高速缓存、电源接口、主从设置电路、数据接口、控制电路板。

  • 主控芯片
    :是机械硬盘的核心控制器,负责管理硬盘的整体操作。它控制数据的读写、定位磁头、管理电机旋转等。
  • 高速缓存
    :是一块用于临时存储数据的内存。它能够提高读取和写入数据的效率,尤其在处理频繁访问的数据时。
  • 电源接口
    : 为硬盘提供电力
  • 主从设置电路
    : 用于配置硬盘在IDE链路上的身份。
  • 数据接口
    :用于与主板或控制器通信的接口

(2)内部结构

机械硬盘的内部结构主要有:磁头组件、盘片、主轴组件、电机、接口等

  • 磁头组件
    : 负责在盘片上读取和写入数据。它是机械硬盘中的核心组件之一,通过悬浮在盘片表面上并移动,实现对数据的访问。
  • 盘片
    : 是机械硬盘内的磁性圆盘,数据存储在其表面上。硬盘通常包含多个盘片,它们叠放在主轴上。
  • 主轴组件
    : 包含
    主轴电机
    ,负责旋转盘片。盘片通过主轴组件旋转,磁头组件通过它来读取和写入数据。
  • 电机
    :提供硬盘所需的动力,主要包括驱动主轴组件的主轴电机和控制磁头的定位电机。

(3) 盘面、磁道、柱面与扇区

我们用一张唱片的盘面来类比硬盘的的盘面,可以看到上面有明显的盘面、磁道、柱面,机械硬盘的内部盘面也类似。

盘面(Platter):

  • 盘面是硬盘内部的物理圆盘,通常由非磁性材料制成,表面涂有可磁化的材料,用于存储数据。
  • 一个硬盘通常包含多个盘面,它们叠在一起并通过主轴电机旋转

磁道(Track):

  • 磁道是盘面上的一个圆形路径,从盘片的中心延伸到边缘。数据在硬盘上以磁道的形式存储
  • 一个盘面上通常有多个同心圆状的磁道,每个磁道上的数据可以被磁头读取或写入

柱面(
Cylinder
)

  • 柱面是多个盘面上相同磁道的集合,它们在垂直方向上对齐,构成一个立体的数据存储结构
  • 通过沿着同一柱面的不同盘面上的磁道进行读写,可以提高数据的访问效率

扇区(Sector):

  • 扇区是磁道上的一个小块,是硬盘中最小的数据存储单元。数据按磁道和扇区的组合方式进行存储。
  • 一个磁道上通常被划分成多个扇区,每个扇区可以存储一定容量的数据,通常是512字节或4K字节

(4)机械硬盘工作原理

1. 盘片旋转:

  • 机械硬盘内部有多个盘片,这些盘片通过主轴组件旋转。主轴电机提供动力,使得盘片以高速旋转,通常以每分钟转数(RPM)来度量。

2. 数据的磁性存储:

  • 每个盘片的表面被分成许多磁道和扇区。数据以磁性的形式存储在这些磁道和扇区上。磁道是圆形的环,而扇区是磁道上的一个小片段。

3. 磁头的运动:

  • 机械硬盘中有多个磁头,每个磁头对应一个盘片的表面。磁头悬浮在盘片表面上,可以通过机械臂的运动进行定位。
  • 机械臂负责将磁头移动到指定的磁道上,以读取或写入数据。磁头的精确定位是机械硬盘关键的技术之一。

4. 读写数据:

  • 当需要读取数据时,控制电路会指示磁头移动到特定的磁道上,并从磁盘上的扇区读取磁性信息。这些信息会被转换为电信号,传递到计算机系统进行处理。
  • 当需要写入数据时,磁头会根据指令移动到指定位置,然后将电信号转换为磁性信息,写入到磁盘的相应扇区。

(5)寻址模式

CHS寻址模式(Cylinder-Head-Sector)

  • 在CHS寻址模式中,数据的物理位置由柱面(Cylinder)、磁头(Head)和扇区(Sector)三个参数确定
  • 计算机使用上面参数来指定硬盘上的数据位置,每个柱面上的磁道由多个扇区组成,磁头负责在柱面上移动,读取或写入指定磁道的指定扇区
  • 这种寻址方式已经逐渐过时,因为它在处理大容量硬盘时存在限制,难以管理大量柱面、磁头和扇区的信息

逻辑块地址(LBA)寻址模式

  • LBA是一种更为直观和简化的寻址方式
  • 在LBA寻址模式中,硬盘被视为一个线性的地址空间,每个扇区都有一个唯一的逻辑编号
  • 计算机系统直接使用这个逻辑块地址来访问硬盘上的数据,而无需考虑具体的物理位置
  • LBA寻址模式对于大容量硬盘和简化操作系统的数据管理来说更为方便,因为它不涉及复杂的CHS参数
  • 现代计算机系统几乎都使用LBA寻址模式,这种方式提供了更高的灵活性和可扩展性,同时简化了硬盘管理

(五)分区格式化

我们买的U盘、SD卡等存储设备,一般我们买回来就可以直接使用,并不需要做什么分区格式化。那是因为在设备出厂之前,厂家已经预先做了这些工作。

一个存储设备在出厂之前,一般都是需要做:

  1. 低级格式化
  2. 选择分区方式
  3. 建立分区
  4. 格式化

详细的分区格式化我们在下一章在介绍。

结尾

计划将文件系统相关的知识点整理输出成一个系列的文章:存储介质介绍、分区格式化、系统启动、FAT系列文件系统、ext系列文件系统、NTFS文件系统、嵌入式文件系统、文件IO等

---------------------------End---------------------------
如需获取更多内容
请关注 liwen01 公众号

概述

在Python中,特殊方法(也称为魔术方法)是由Python解释器自动调用的,我们不需要手动调用它们,而是使用内置函数来间接地使用它们。举个例子,我们可以实现特殊方法
__len__()
,然后通过使用内置函数len()来获取对象的长度。同样地,一些特殊方法的调用是隐式的,比如在使用for循环时,实际上是在使用内置函数iter(x)来迭代对象x。

在Java中,并没有像Python那样简单的实现方式。如果要进行比较,你需要实现compare方法;如果想要生成特定的对象,必须编写适当的构造器方法;如果想要打印对象,就需要实现toString方法等等。此外,在Java中的魔术类或方法以及各种本地方法基本上是不允许我们修改的。因此,我们还是简单地看一下Python是如何调用的吧。

常用特殊方法及实现

下面将介绍一些常用特殊方法和实现。

__init__(self, ...)

构造函数主要用于初始化对象的属性。通过在类中定义__init__方法,可以在创建对象时自动调用该方法来初始化对象的属性。在构造函数中,可以通过self参数来访问对象的属性和方法,并为其赋予初始值。构造函数在对象创建时只会被调用一次,可以在其中执行一些必要的初始化操作,以确保对象在创建后处于正确的状态。

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

person = Person('xiaoyu', 18)
print(person.name)  # 输出: xiaoyu
print(person.age)   # 输出: 18

__str__(self)

返回对象的字符串表示,可通过print(obj)来调用。这个方法对于调试和输出对象信息非常有用,可以提供更友好和可读性更高的输出结果。通过实现__str__方法,我们可以轻松地控制对象的打印行为,使其更符合我们的需求和预期。

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"Person: {self.name}, {self.age} years old"

person = Person('xiaoyu', 18)
print(person)  # 输出: Person: xiaoyu, 18 years old

__repr__(self)

返回对象的可打印字符串表示,可通过obj来调用。与
__str__
方法类似,
__repr__
方法也用于提供对象的字符串表示,但它的目标是更加精确和完整地描述对象的内部状态和结构。

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __repr__(self):
        return f"Person(name={self.name}, age={self.age})"

person = Person('xiaoyu', 18)
print(person)  # 输出: Person(name=xiaoyu, age=18)

__len__(self)

返回对象的长度,可通过len(obj)来调用。在Python中,len()函数通常用于获取容器对象的元素个数,比如列表、字符串、字典等。通过实现
__len__
方法,我们可以自定义对象的长度,并使其适应于特定的应用场景。例如,如果我们定义了一个自定义的容器类,可以通过实现
__len__
方法来返回容器中元素的个数。这样,我们就可以像对待其他内置容器一样,使用len()函数来获取自定义容器对象的长度。

class MyList:
    def __init__(self):
        self.data = []

    def __len__(self):
        return len(self.data)

my_list = MyList()
my_list.data = [1, 2, 3, 4, 5]
print(len(my_list))  # 输出: 5

__getitem__(self, key)

获取对象的指定元素,可通过obj[key]来调用。并将键值key传递给它作为参数。

class MyList:
    def __init__(self):
        self.data = []

    def __getitem__(self, key):
        return self.data[key]

my_list = MyList()
my_list.data = [1, 2, 3, 4, 5]
print(my_list[2])  # 输出: 3

__setitem__(self, key, value)

设置对象的指定元素,通过重载该方法,可以实现通过obj[key] = value来调用该方法。

class MyList:
    def __init__(self):
        self.data = []

    def __setitem__(self, key, value):
        self.data[key] = value

my_list = MyList()
my_list.data = [1, 2, 3, 4, 5]
my_list[2] = 10
print(my_list.data)  # 输出: [1, 2, 10, 4, 5]

__delitem__(self, key)

删除对象的指定元素,可通过del obj[key]来调用。当你想要从对象中删除特定的元素时,这个方法会非常有用。

class MyList:
    def __init__(self):
        self.data = []

    def __delitem__(self, key):
        del self.data[key]

my_list = MyList()
my_list.data = [1, 2, 3, 4, 5]
del my_list[2]
print(my_list.data)  # 输出: [1, 2, 4, 5]

__iter__(self)

返回一个可迭代对象,以便我们可以通过for item in obj的方式进行迭代。

class MyList:
    def __init__(self):
        self.data = []

    def __iter__(self):
        return iter(self.data)

my_list = MyList()
my_list.data = [1, 2, 3, 4, 5]
for item in my_list:
    print(item)  # 输出: 1 2 3 4 5

__next__(self)

返回迭代器的下一个元素,可通过next(obj)来调用。

class MyIterator:
    def __init__(self):
        self.data = [1, 2, 3, 4, 5]
        self.index = 0

    def __next__(self):
        if self.index >= len(self.data):
            raise StopIteration
        item = self.data[self.index]
        self.index += 1
        return item

my_iterator = MyIterator()
print(next(my_iterator))  # 输出: 1
print(next(my_iterator))  # 输出: 2
print(next(my_iterator))  # 输出: 3

__call__(self, ...)

将对象作为函数调用时触发,可通过obj(...)来调用。

class MyFunc:
    def __call__(self, x, y):
        return x + y

my_func = MyFunc()
result = my_func(3, 4)
print(result)  # 输出: 7

__eq__(self, other)

判断对象是否相等,可通过obj == other来调用。在Java中如果判断对象引用的话,你也需要重写equals这个方法。

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

point1 = Point(1, 2)
point2 = Point(1, 2)
print(point1 == point2)  # 输出: True

__lt__(self, other)

判断对象是否小于另一个对象,可通过obj < other来调用。

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __lt__(self, other):
        return self.x < other.x and self.y < other.y

point1 = Point(1, 2)
point2 = Point(3, 4)
print(point1 < point2)  # 输出: True

__gt__(self, other)

判断对象是否大于另一个对象,可通过obj > other来调用。

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __gt__(self, other):
        return self.x > other.x and self.y > other.y

point1 = Point(3, 4)
point2 = Point(1, 2)
print(point1 > point2)  # 输出: True

__add__(self, other)

定义对象的加法操作,可通过obj + other来调用。这样的话,你也可以对对象进行进行运算操作了

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

vector1 = Vector(1, 2)
vector2 = Vector(3, 4)
result = vector1 + vector2
print(result.x, result.y)  # 输出: 4 6

__sub__(self, other)

定义对象的减法操作,可通过obj - other来调用。

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)

vector1 = Vector(3, 4)
vector2 = Vector(1, 2)
result = vector1 - vector2
print(result.x, result.y)  # 输出: 2 2

常用特殊(魔术)方法的汇总一览表

以下是20个常用的特殊方法及其调用方式的表格,再多的用到了我们在看一下就可以了,只要记住(__)这个下划线开头和结尾的方法就可。请看下表:

特殊方法 描述 调用方式
__init__ 初始化对象 obj = ClassName()
__str__ 返回对象的字符串表示 str(obj)
__len__ 返回对象的长度 len(obj)
__getitem__ 获取对象的元素 obj[index]
__setitem__ 设置对象的元素 obj[index] = value
__delitem__ 删除对象的元素 del obj[index]
__contains__ 判断对象是否包含某个元素 value in obj
__add__ 实现对象的加法 obj1 + obj2
__sub__ 实现对象的减法 obj1 - obj2
__mul__ 实现对象的乘法 obj1 * obj2
__div__ 实现对象的除法 obj1 / obj2
__eq__ 判断对象是否相等 obj1 == obj2
__ne__ 判断对象是否不相等 obj1 != obj2
__lt__ 判断对象是否小于 obj1 < obj2
__gt__ 判断对象是否大于 obj1 > obj2
__le__ 判断对象是否小于等于 obj1 <= obj2
__ge__ 判断对象是否大于等于 obj1 >= obj2
__iter__ 返回一个迭代器对象 iter(obj)
__next__ 返回迭代器的下一个元素 next(obj)
__del__ 对象销毁时调用 del obj
__call__ 将对象作为函数调用 obj()

总结

Python中的特殊方法(魔术方法)是由Python解释器自动调用的,用于实现对象的特定功能。常用的特殊方法包括
__init__

__str__

__repr__

__len__

__getitem__

__setitem__

__delitem__

__iter__

__next__

__call__
等。通过实现这些特殊方法,我们可以自定义对象的行为,使其更加符合我们的需求和预期。这些特殊方法的调用方式也有所不同,可以通过内置函数、运算符、迭代等方式来调用。掌握了这些特殊方法的用法,可以提高我们在Python中编写类和对象的灵活性和可扩展性。