普通视图

发现新文章,点击刷新页面。
昨天以前首页

韦爵爷闯荡 Swift 6 江湖:单例秘籍新解(上)

2025年9月2日 14:16

在这里插入图片描述

引子

话说康熙年间,iOS 江湖正值 “版本迭代” 的乱世 ——Swift 6 携 “并发安全” 的大旗横空出世,往日里被开发者奉为 “偷懒神器” 的单例招式,一夜之间成了 “违规禁招”。

天地会总舵主陈近南刚收到线报,不少分舵兄弟因沿用旧单例写法,导致 App 频繁闪退,连给康熙爷呈递的 “奏章 App” 都差点出了纰漏。

在这里插入图片描述

深知此事关乎帮派声誉,陈近南连夜将韦小宝召至总舵,递上一本封皮烫金的《Swift 6 单例秘籍》,沉声道:“小宝,你在皇宫里见多识广,这单例就像你藏的‘四十二章经’,既是刚需又藏着风险。如今 Swift 6 立下新规矩,若再用老办法,轻则丢了差事,重则连累天地会!”

在本篇帮规中,各位少侠将学到如下内容:

  • 引子
  • 📜 第一章:单例的 “江湖往事”—— 为何人人又爱又恨?
  • ⚖️ 第二章:Swift 6 的新铁规 —— 为何单例突然 “违法”?

韦爵爷虽不懂高深代码武功,却凭着 “察言观色、见招拆招” 的本事,决定先把这单例的 “前世今生” 和新规矩摸个通透。


📜 第一章:单例的 “江湖往事”—— 为何人人又爱又恨?

在 Swift 旧江湖里,单例是绝对的 “流量明星”。

比如天地会要验证兄弟身份,得有个 “身份验证总舵”(AuthProvider);骁骑营要统计全军战力,得有个 “战力账本”(GamePiece);甚至韦小宝的 “丽春院记账 App”,都得有个 “全局账本实例”—— 总不能让每个模块都单独建个 “总舵” 或 “账本”,那岂不是乱了套?

在这里插入图片描述

老江湖写单例,向来简单粗暴,如同韦爵爷掏匕首般干脆:

class AuthProvider {
    // 全局共享的“总舵印信”,全江湖就这一个
    static let shared = AuthProvider()
    // 存储兄弟令牌,验证身份时要用
    private var memberTokens: [String: Bool] = [:]
    
    // 验证令牌是否有效——这招是总舵核心功夫
    func verifyToken(_ token: String) -> Bool {
        return memberTokens[token] ?? false
    }
    
    // 私有初始化,防止外人仿造“印信”——这点很关键!
    private init() {}
}

用的时候更方便,各分舵兄弟随用随调:

// 杭州分舵验证令牌

let isValid = AuthProvider.shared.verifyToken("天地会-杭州-001")

可这单例的 “坏名声”,比韦爵爷的 “流氓行径” 传得还快。最大的问题就是 “全局可变状态”—— 就像那本 “战力账本”,若是两个小兵同时修改:

// 小兵甲在主线程给战力加10
GamePiece.shared.power += 10
// 小兵乙在后台线程给战力减5
DispatchQueue.global().async {
    GamePiece.shared.power -= 5
}

结果可能是战力值算错(数据竞态),严重时直接让 “战力统计 App” 闪退 —— 这就像韦小宝算错丽春院账目,轻则被老妈子骂,重则赔光家底。

在这里插入图片描述

不过江湖前辈们早有对策:改用 “显式依赖注入”(DI),如同韦小宝办事前先亮身份、交凭证,明明白白不藏私。比如给杭州分舵 “送” 一个 AuthProvider 实例,而非让他们直接抢 “总舵印信”:

// 给杭州分舵传一个 AuthProvider 实例
class HangzhouBranch {
    private let auth: AuthProvider
    // 初始化时明确传入“身份验证工具”,不偷偷用全局单例
    init(auth: AuthProvider) {
        self.auth = auth
    }
    
    // 用传入的实例验证令牌,安全可控
    func checkMemberToken(_ token: String) -> Bool {
        return auth.verifyToken(token)
    }
}

// 使用时主动创建实例并传入
let auth = AuthProvider()
let hangzhouBranch = HangzhouBranch(auth: auth)

此法虽麻烦,却能避免 “全局状态混乱”,代码的 “可测试性” 也大大提升 —— 就像韦小宝做账时留凭证,查账时一目了然。

⚖️ 第二章:Swift 6 的新铁规 —— 为何单例突然 “违法”?

虽说显式依赖注入是正道,但江湖总有 “不得不⽤单例” 的场景 —— 比如 “系统时间工具”、“网络请求管理器”,全 App 只能有一个实例,否则会出大问题。

可到了 Swift 6 这新江湖,连这种 “刚需单例” 都遭了殃。

在这里插入图片描述

韦爵爷照着老写法敲完代码,刚一运行,编译器就弹出一道刺眼的红牌,如同九门提督捕快举着的 “通缉令”:

“Static property 'shared' is not concurrency-safe because it is nonisolated global shared mutable state”

(翻译过来就是:静态属性 “shared” 不安全!因为它是 “没靠山(未隔离)” 的全局可变状态)

韦爵爷气得直拍桌子:“我这‘shared’用的是‘let’,又不是‘var’,怎么就‘可变’了?编译器是不是跟我过不去?”

陈近南忙递上一杯茶,指着代码解释:“小宝莫急,你看这 AuthProvider 里的‘memberTokens’—— 它是‘var’类型,能随时修改。虽然‘shared’本身是‘let’(实例不变),但实例内部的状态能变啊!就像你手里的匕首(实例)不变,但匕首能捅人(内部状态修改),照样有风险。”

他又翻出骁骑营的 “战力账本” 代码,更是一目了然:

// 骁骑营战力账本——用了 static var,直接遭红牌
class GamePiece {
    // 编译器红牌:非并发安全!
    static var power = 100 // 全局可变的战力值,谁都能改
}

Swift 6 最看重‘并发安全’,” 陈近南继续说道,“它怕的是‘多线程同时修改状态’—— 就像两个小兵同时改变战力值,一个加 10,一个减 5,最后账本上可能不是 105,而是 95 或者 110(数据竞态)。这种‘暗箱操作’,在新江湖里绝对不允许!”

韦爵爷这才恍然大悟:“原来不是编译器针对我,是这新规矩管得严!那我该怎么改?总不能不用单例吧?”

在这里插入图片描述

陈近南笑着摇头:“倒也不是不能用,只是得按新规矩来。要破这‘非并发安全’的罪名,得分两种情况:一种是‘实例内部状态不变’(比如纯工具类,只做计算不存数据),改个小写法就能过;另一种是‘实例内部要变’(比如战力值、令牌库),就得给它找个‘靠山’—— 要么投靠‘全局演员’(MainActor),要么用‘unsafe 免死金牌’,还有更稳妥的‘actor 独行侠’路子。

韦爵爷听得眼睛发亮:“这么多门道?快给我说说第一种情况,怎么改个小写法就能过?”

陈近南却故意卖起关子:“小宝别急,这‘改写法’虽简单,却藏着新坑 —— 比如改成‘static let’后,编译器可能又会甩出‘非 Sendable 类型’的新罪名。而且‘找靠山’的三种门道,各有优劣,得细细拆解才不会踩坑。咱们下篇就从‘静态变量改常量’的解法说起,再对比‘投靠 MainActor’和‘用 unsafe 免死牌’的利弊,保准让你把第一重难关的出路摸得明明白白。”

在这里插入图片描述

欲知 “static let 如何化解第一道红牌”,“非 Sendable 新罪名又是什么来头”,且看下篇分解。

前端使用次数最多的工具封装

作者 一支鱼
2025年9月1日 23:38

1. 数据处理工具

1.1 数组去重并保持顺序

  • 功能分析:去除数组中的重复元素,并保持元素的原始顺序。在处理需要保持特定顺序的数据,如用户操作历史记录等场景中很有用。
  • 代码实现
const arrayUtils = {
    uniqueArrayPreserveOrder: function <T>(arr: T[]): T[] {
        const seen: { [key: string]: boolean } = {};
        return arr.filter((item) => {
            const key = typeof item === 'object'? JSON.stringify(item) : item;
            if (seen[key]) {
                return false;
            }
            seen[key] = true;
            return true;
        });
    }
};
  • 使用示例
const arrWithDuplicates = [1, { value: 'a' }, 2, { value: 'a' }, 3];
const uniqueArr = arrayUtils.uniqueArrayPreserveOrder(arrWithDuplicates);
console.log(uniqueArr); 
// 输出: [1, { value: 'a' }, 2, 3]

1.2 数组分组

  • 功能分析:根据给定的分组函数,将数组元素分成不同的组。常用于数据统计、分类展示等场景,比如将订单按月份分组。
  • 代码实现
const arrayUtils = {
    groupArray: function <T, K>(arr: T[], keySelector: (item: T) => K): { [key: string]: T[] } {
        return arr.reduce((acc, item) => {
            const key = keySelector(item).toString();
            if (!acc[key]) {
                acc[key] = [];
            }
            acc[key].push(item);
            return acc;
        }, {} as { [key: string]: T[] });
    }
};
  • 使用示例
const orders = [
    { id: 1, date: new Date('2024 - 01 - 10'), amount: 100 },
    { id: 2, date: new Date('2024 - 02 - 15'), amount: 200 },
    { id: 3, date: new Date('2024 - 01 - 20'), amount: 150 }
];
const groupedOrders = arrayUtils.groupArray(orders, order => order.date.getMonth());
console.log(groupedOrders); 
// 输出: { '0': [ { id: 1, date: 2024 - 01 - 10, amount: 100 }, { id: 3, date: 2024 - 01 - 20, amount: 150 } ], '1': [ { id: 2, date: 2024 - 02 - 15, amount: 200 } ] }

2. DOM 操作工具

2.1 获取元素距离视口顶部的距离

  • 功能分析:获取元素相对于浏览器视口顶部的距离,对于实现视口相关的交互,如元素进入视口时触发动画等功能很重要。
  • 代码实现
const domUtils = {
    getElementTopRelativeToViewport: function (element: HTMLElement): number {
        const rect = element.getBoundingClientRect();
        return rect.top + window.pageYOffset;
    }
};
  • 使用示例
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>DOM Utils Example</title>
    <style>
        #testDiv {
            margin - top: 200px;
        }
    </style>
</head>

<body>
    <div id="testDiv">Test Div</div>
    <script lang="ts">
        const testDiv = document.getElementById('testDiv') as HTMLElement;
        const divTop = domUtils.getElementTopRelativeToViewport(testDiv);
        console.log(divTop); 
    </script>
</body>

</html>

2.2 批量添加事件监听器

  • 功能分析:为多个元素一次性添加相同类型的事件监听器,简化事件绑定操作,提高代码效率。适用于批量处理同类元素的交互,如按钮组的点击事件。
  • 代码实现
const domUtils = {
    addEventListeners: function (selector: string, eventType: string, handler: (event: Event) => void) {
        const elements = document.querySelectorAll(selector);
        elements.forEach((element) => {
            element.addEventListener(eventType, handler);
        });
    }
};
  • 使用示例
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>DOM Utils Example</title>
</head>

<body>
    <button class="actionButton">Button 1</button>
    <button class="actionButton">Button 2</button>
    <script lang="ts">
        domUtils.addEventListeners('.actionButton', 'click', (event) => {
            console.log('Button clicked');
        });
    </script>
</body>

</html>

3. 网络请求工具(基于 Axios)

3.1 带重试机制的网络请求封装

  • 功能分析:在网络请求失败时,自动进行重试,提高请求的成功率。对于不稳定的网络环境或偶尔出现的短暂故障很实用。
  • 代码实现
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';

interface ResponseData<T> {
    code: number;
    message: string;
    data: T;
}

const httpUtils: {
    instance: AxiosInstance;
    init: () => void;
    get: <T>(url: string, params?: object, retries = 3): Promise<T>;
    post: <T>(url: string, data?: object, retries = 3): Promise<T>;
    put: <T>(url: string, data?: object, retries = 3): Promise<T>;
    delete: <T>(url: string, retries = 3): Promise<T>;
} = {
    instance: axios.create({
        baseURL: 'https://your - api - base - url.com',
        timeout: 5000,
        headers: {
            'Content - Type': 'application/json'
        }
    }),
    init: function () {
        this.instance.interceptors.request.use((config: AxiosRequestConfig) => {
            const token = localStorage.getItem('token');
            if (token) {
                config.headers.Authorization = `Bearer ${token}`;
            }
            return config;
        }, error => {
            return Promise.reject(error);
        });

        this.instance.interceptors.response.use((response: AxiosResponse<ResponseData<any>>) => {
            if (response.data.code!== 200) {
                throw new Error(response.data.message);
            }
            return response.data.data;
        }, error => {
            console.error('Request error:', error);
            return Promise.reject(error);
        });
    },
    get: async function <T>(url: string, params: object = {}, retries = 3): Promise<T> {
        let attempt = 0;
        while (attempt < retries) {
            try {
                const response = await this.instance.get(url, { params });
                return response.data;
            } catch (error) {
                attempt++;
                if (attempt === retries) {
                    throw error;
                }
            }
        }
        throw new Error('Max retries reached');
    },
    post: async function <T>(url: string, data: object = {}, retries = 3): Promise<T> {
        let attempt = 0;
        while (attempt < retries) {
            try {
                const response = await this.instance.post(url, data);
                return response.data;
            } catch (error) {
                attempt++;
                if (attempt === retries) {
                    throw error;
                }
            }
        }
        throw new Error('Max retries reached');
    },
    put: async function <T>(url: string, data: object = {}, retries = 3): Promise<T> {
        let attempt = 0;
        while (attempt < retries) {
            try {
                const response = await this.instance.put(url, data);
                return response.data;
            } catch (error) {
                attempt++;
                if (attempt === retries) {
                    throw error;
                }
            }
        }
        throw new Error('Max retries reached');
    },
    delete: async function <T>(url: string, retries = 3): Promise<T> {
        let attempt = 0;
        while (attempt < retries) {
            try {
                const response = await this.instance.delete(url);
                return response.data;
            } catch (error) {
                attempt++;
                if (attempt === retries) {
                    throw error;
                }
            }
        }
        throw new Error('Max retries reached');
    }
};

httpUtils.init();
export default httpUtils;
  • 使用示例
import httpUtils from './httpUtils';

// GET请求
httpUtils.get('/api/data', { param1: 'value1' }, 5)
  .then(data => {
        console.log(data);
    })
  .catch(error => {
        console.error(error);
    });

4. 存储管理工具

4.1 本地存储有效期管理

  • 功能分析:为本地存储的数据设置有效期,过期后自动删除。适用于存储一些临时数据,如用户登录的短期凭证等。
  • 代码实现
const storageUtils = {
    setLocalStorageWithExpiry: function (key: string, value: any, durationInMinutes: number) {
        const data = {
            value,
            expiry: new Date().getTime() + durationInMinutes * 60 * 1000
        };
        localStorage.setItem(key, JSON.stringify(data));
    },
    getLocalStorageWithExpiry: function (key: string): any {
        const item = localStorage.getItem(key);
        if (!item) {
            return null;
        }
        const { value, expiry } = JSON.parse(item);
        if (new Date().getTime() > expiry) {
            localStorage.removeItem(key);
            return null;
        }
        return value;
    }
};
  • 使用示例
storageUtils.setLocalStorageWithExpiry('tempToken', 'abc123', 30); 
const tempToken = storageUtils.getLocalStorageWithExpiry('tempToken');
console.log(tempToken); 

5. 日期时间处理工具

5.1 获取指定月份的所有日期

  • 功能分析:获取指定年份和月份的所有日期,在日历组件开发等场景中经常用到。
  • 代码实现
const dateTimeUtils = {
    getDatesOfMonth: function (year: number, month: number): Date[] {
        const dates: Date[] = [];
        const lastDay = new Date(year, month + 1, 0).getDate();
        for (let day = 1; day <= lastDay; day++) {
            dates.push(new Date(year, month, day));
        }
        return dates;
    }
};
  • 使用示例
const dates = dateTimeUtils.getDatesOfMonth(2024, 9); 
dates.forEach(date => {
    console.log(date.toISOString().split('T')[0]);
});
// 输出2024年10月的所有日期

5.2 计算两个日期之间的工作日天数

  • 功能分析:计算两个日期之间的工作日天数,不包括周末。在项目进度管理、考勤计算等场景中有实际应用。
  • 代码实现
const dateTimeUtils = {
    getWorkingDaysBetween: function (startDate: Date, endDate: Date): number {
        let currentDate = new Date(startDate);
        let workingDays = 0;
        while (currentDate <= endDate) {
            const dayOfWeek = currentDate.getDay();
            if (dayOfWeek!== 0 && dayOfWeek!== 6) {
                workingDays++;
            }
            currentDate.setDate(currentDate.getDate() + 1);
        }
        return workingDays;
    }
};
  • 使用示例
const start = new Date('2024 - 10 - 01');
const end = new Date('2024 - 10 - 10');
const workingDays = dateTimeUtils.getWorkingDaysBetween(start, end);
console.log(workingDays); 
// 计算2024年10月1日到10月10日之间的工作日天数
❌
❌