65.9K
CodeProject 正在变化。 阅读更多。
Home

标签和其他相关杂项

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (6投票s)

2012年10月22日

CPOL

20分钟阅读

viewsIcon

12571

downloadIcon

2

一个男人用代码整理甜饼怪图片的心路历程……

引言

在互联网上,打标签是相当普遍的做法,尤其是在博客网站上。它消除了诸如“这个该放进哪个文件夹?”之类的烦恼,并使得搜索图片、视频、博客、丑陋的怪物、音乐及其他有用数据变得相当快捷高效。

然而,在台式电脑上,这并不常见。某些文件格式,如 JPEG 和 MP3,原生支持标签,但并**不是所有**文件格式都支持。这意味着,如果你想为你所有的甜饼怪图片打上标签(*别撒谎,我知道你有一些*),你就必须确保每一张图片都是“*甜饼怪.jpg*”,而不是“*甜饼怪.png*”。

我想大多数人不会这么做;这意味着为了找到他们的图片,他们会把图片放进相关的文件夹里;比如“甜饼怪”、“艾摩”、“大鸟”等等。这时,那个可怕的、可怕的“第一世界问题”就来了:“等等!如果我有一张同时有甜饼怪**和**艾摩的图片该怎么办?我该怎么做?”。

所以,在图片文件夹可能变得一团糟的威胁下,我启动了 Visual Studio,写了一个名为 ASH 的程序。这个程序管理一个给定文件夹中所有图片的数据库,并为每张图片分配标签。为什么叫“ASH”?因为当时我正在看《鬼玩人2》,而 Ash 这个名字似乎足够随意,可以用作程序名。而且我忘了关掉大写锁定键。而且这名字听起来有点神秘。是啊……

ASH 支持从其数据库中添加、删除、修改和搜索图片,这意味着你只需搜索“蓝色怪物”就可以相对高效地找到你的甜饼怪图片。

为什么不用[在此插入更高级的数据库软件]?

有几个原因,没有一个真正理性的(或者说好的)

  1. [在此插入更高级的数据库软件]是给无聊的办公室职员用的
  2. [在此插入更高级的数据库软件]的开发者们(错误地)认为《鬼玩人》三部曲非常、非常蹩脚且预算不足
  3. [在此插入更高级的数据库软件]不理解《芝麻街》的重要性,并把伯特和厄尼视为“仅仅是数据库中的一个条目”(并不是说 ASH 就理解了[暂时还没有],但这是值得思考的事情。)

背景

ASH 的初期开发大约花了2周时间,大部分是在凌晨一点,期间还喝了无数次激浪汽水和各种拖延。它的结构是抄袭自模仿 Booru 网站(或者至少,是我猜测 Booru 网站可能的工作方式),所以从一开始我就把它构建成一种服务器类型的东西,而不是一个独立程序。

ASH 的核心是基于一种自定义数据库格式,它由一个9字节的头部和随后的1千字节“块”组成。每个块包含一个260字节的路径、一个8字节的时间戳和756字节的标签。(我算术不行,所以这个数可能不对,但无所谓了)。在文件中,任何未使用的数据都设置为0,但如果只用了百来个字节却要加载整整一千字节的数据,这显然是巨大的浪费,所以在内存中,数据被写入一个 Block 对象。

class Block
{
public:
    // ...
private:
    // ...
    bool pending;
    int block_number;
    unsigned long long time_stamp;
    char * fl_path;
    Tag fl_tags; 
};

有两样东西没有直接编码在文件中,它们是 pendingblock_number;前者只是用来跟踪哪些 Block 真正需要写入磁盘,而 block_number 是用来指定写入信息的位置(实际的字节偏移量是通过将 block_number 乘以1024,然后加上9得出的;就是我在数学课上没注意听的那些线性方程之类的东西)。time_stamp 是按原样加载和写入的(尽管有一些小代码来确保字节以正确的顺序读出)。fl_path 的处理方式也和人们预期的一样;分配一个缓冲区,然后把它转储到内存中,只是没有多余的零。

另一方面,fl_tags 的处理方式则大不相同。

《奇爱博士》或:我如何学会停止恐惧并爱上炸弹

如果你随便上一个博客,你可能会注意到(除了博客本身,你懂的)它有一个标签云;就是屏幕一侧那个小框框,里面有一大堆词。当然,在开发 ASH 时,我想加入类似的功能,但我也想确保在这些标签中搜索既快速又高效。

这时,C++ 标准模板库及其 std::map<> 模板类登场了。

乍一看,我想“哦,哇!这**正是我需要的**!一个将键映射到值的对象!”。我开始将 std::map 集成到 ASH 中,并编写了加载代码来解析单个标签并添加它们。每个标签都会链接到一个 Block。整个过程中,我当然真的以为它会完美运行,并极大地减少搜索图片所需的时间。

然后我实际运行了它。

它**慢得要死**。加载一个500KB的文件足足花了10秒钟。现在,得承认,这可能不是 std::map 的错,我敢肯定有几百万个无聊的、数学上的原因可以解释为什么这种方法拖垮了 ASH,但是数学很蠢。重要的是它很慢,而且使用 std::map 使得进行部分匹配变得极其困难。因此,我开始拥抱炸弹删除键,并暴力拆解 ASH 的闪亮新丑陋旧代码。

第四集:新希望

seconds小时后,是时候开始重建标签系统了,也许还可以加入一些新的、多余的功能,比如“搜索”和“更新标签”。

结果我跑去看了《黑暗军团》。

在看完 Ash 英勇地战胜恶灵,勇敢地阻止他们获取《死灵之书》后,我意识到一件超级重要的事情。如果我不完成这个,**恶灵就会赢**(当时是凌晨四点,我喝了太多激浪汽水。别评判我)。我立即开始重新设计标签系统。起初我想我应该用某种字典完全替换 std::map…… 但我数学很烂,我想出的算法似乎会比我能接受的多占用几个G的内存(我数学**真的**很烂)。这意味着我必须把整个东西都删掉。大约在这个时候,我在我的(纸质)笔记本上开了一个新章节,标题是“新世界秩序”,并开始琢磨新标签系统的具体细节。

结果就是 ASH 目前的 Tag 对象

class Tag
{
public:
    // ...
    int match(const Tag& t) const;
    int match(const char* t, long len=0) const;
private:
    // ...
    long length;
    IAllocate* allocator;
};

新的标签系统在几个方面更好。首先,可能也是最重要的一点,标签在加载后不必被拆分;事实上,它们**永远**不需要被拆分,除非你是个可怕、卑鄙的人,喜欢毁掉它们的生活。

树形结构现在已经没有了,取而代之的是一个数组;算不上惊艳,但速度更快了。此外,Tag 支持部分匹配,以及同时匹配多个标签。

Tag 或多或少就是带有自动清理功能的美化版字符串。length 是标签实际占用的字节数,而 allocator 只是管理它的内存。有点臃肿,但能用。

Tag 带来的最重要的特性是它的匹配方法,它会返回与输入标签的匹配次数,像这样(请原谅我的微软画图“技巧”):

Matching tags

为了节省处理时间和内存,整个操作是就地完成的,没有使用递归(这个图不完全准确)。目前,匹配方法是两个独立的函数,它们做的事情几乎完全一样,主要是因为在我睡了几个小时后,我阻止恶灵获胜的目标稍微淡化了(别评判我!)。

在追加标签时,会使用一个类似但稍微复杂的过程,以确保只追加唯一的标签。其余的都被忽略了。

其他

在某个时候,事情变得很明显,除了以科学的名义彻底搞砸 ASH,我还必须编写自定义的分配器。为什么?因为我能(当然也是因为我想尽量减少重新调整大小的次数)。

class DefAllocator: public IAllocate
{
public:
    // ...
    void alloc(long s);
    void alloc_exact(long s);
    // ...
private:
    long sz, rz;
    byte * data;
};

默认分配器根据请求的空间大小(s)和重新调整大小的次数(rz)来调整 data 的大小。

long size = ((s*(++rz)*rz) >> 1);
if(size < s) size = (s + rz) + 1;

其理论是,浪费内存比浪费CPU要好。

列表,列表,列表,还有……蔓越莓汁?!

Ash::list 的初衷是成为 std::vector 的一个非常非常臃肿的版本(但我向你保证,是以最好的方式)。与 Ash::DefAllocator 的思路非常相似,list 分配越来越多的内存,以减少重新调整大小操作的次数。因为在标签系统那次可怕的爆炸重构事件之后,我试图尽量少做改动,所以 list 看起来就像是 vector 的一个巨大山寨品。

template<typename item>
class list
{
public:
    // ...
private:
    // Boring internal junk no one cares about...

    long size, mem_count;
    long resizes;
    item* members;
    SizeType st;
};

关于 list 唯一特别独特的地方是,它允许你改变它的大小调整方式(SizeType),这样你就可以防止不经常调整大小的 list 失控增长。

管道、随机数生成器和 GNU/Linux

如果你粗略地看一下 ASH 的源代码,你可能会注意到一些对 GNU/Linux 和 GCC 的引用,然后在几分钟后,当你发现 ASH 在 *nix 系统下无法(或者说,不再能)编译时,会开始愤怒地砸你的显示器/键盘/鼠标/任何东西。这纯粹是懒惰。我还没写那部分代码。

然而,这意味着 ASH 中那些不能只用标准库实现的部分都有接口;特别是 IPipeServerIRandomProviderIAllocate 也是一个接口,但它不算,因为它不是出于跨平台原因而这样实现的[别问])。

IPipeServer 基本上只是相关系统调用的一个包装器。IRandomProvider 有点不同,因为它有三个独立的实现。第一个是 DefRandomProvider,它实际上是标准 srand()rand() 函数的包装器;这有点没用,因为标准库本身就已经是跨平台的了。另外两个是 WinRandomProviderNixRandomProvider,它们提供了比 DefRandomProvider 更具加密安全性的方法。在 Windows 上,这是一个 CryptGenRandom() 的包装器,并处理一些重要的杂事,比如获取加密上下文和抛出明目张胆的异常。在 *nix 系统上,会从 /dev/random 读取一个种子,然后从中生成随机数(因为为了几KB的数据花15分钟确实很荒谬)。

法西斯主义

作为又一个实验,我加入了 AccessControl 来管理登录。

class AccessControl
{
public:
    // ...

    void generateLogin(Token& out);
    bool isLoggedIn(const Token& in) const;
    bool logoutUser(const Token& in);
 
    // ...
private:
    IRandomProvider * irp;
    list<Token> logins;
};

AccessControl 生成32字节的会话令牌,很可能可以用来管理单个会话(我承认,这在技术上还没测试过……)。我确定这在某些地方会很重要。

整合

可惜,我想不出办法把《音乐之声》融入这一节。抱歉。

你现在可能已经猜到了,Block 们并非自行其是。它们由
一个单独的 File 对象管理

class File
{
public:
    // ...
private:
    // ...
    int header_sz;
    std::fstream file_obj;
    list<Block> file_blocks;
};

header_sz 是 ASH 头的大小。还记得我说过它是9个字节吗?嗯……我撒了点谎(一点点)。ASH 数据库中的第一个字节是头的总大小,不包括第一个字节本身。在 ASH 的当前版本中,第一个字节总是等于8;不过你实际上可以用十六进制编辑器制作自己的文件,然后把它改成任何你想要的数字。当向磁盘写入一个块时,字节偏移量是这样计算的

偏移量 = header_sz + (block_sz * block_number)

目前,额外的可用头部数据并未使用;这只是为了未来的扩展。

file_obj,如果你还没猜到的话,就是数据库文件本身的文件对象。它在程序执行期间一直存在,以保持数据库文件对外部进程的锁定,不过在程序的大部分时间里它什么也不做。

file_blocks 是从数据库文件加载的 Block 列表。目前,ASH 直接搜索这些块。

使用代码

按原样编译后,ASH 在命令行中运行。首先,它会要求用户(你?)指定 ASH 数据库的完整路径。

如果文件不存在,ASH 会直接创建它(是的,它必须是“.ash”,否则你身后站着的那位可能会生气。不,说真的。别。回。头。)。然后,你可以输入命令让 ASH 执行,具体有:“add”、“remove”、“find”、“find_path”、“tag”和“path”(当然,不带引号)。参数用空格分隔;如果任何单个参数需要包含空格(例如文件路径),请用引号括起来。

  • “add [文件路径] [标签]” 向 ASH 数据库添加一个条目,包含指定的文件名和标签。如果指定的文件名已存在,它会将指定的标签与现有条目合并。标签应以空格分隔。
  • “remove [文件路径]” 从数据库中删除一个条目。
  • “find [标签]” 返回数据库中所有与指定标签匹配的条目列表,按匹配度从高到低排序。由于一个我懒得现在修复的相当烦人的问题,标签应以|竖线|分隔。
  • “find_path [文件路径]” 查找具有指定文件路径的条目并返回其标签。
  • “tag [文件路径] [标签]” 为现有条目添加指定的标签;这与“add”不同,“tag”在条目不存在时不会添加它。标签应以空格分隔。
  • “path” 不带参数,返回数据库的路径(但不包括文件名)。

一系列管子管道

一旦 ASH 建立了一个数据库,它会打开一个名为“AshPipes”的命名管道,以接收来自外部进程的命令。

Controlling ASH from an external process

在这个例子中,实验 #735(别问编号怎么来的,否则那个可怕的家伙会回来)是一个 .NET 进程,它作为 ASH 的一个 GUI 包装器。当用户点击某个东西,比如说“添加图片”,它会创建一个包含“add”命令及其参数的 String,然后发送出去。

private String sendString(String output)
{
    // Exception-handling junk...

    StreamWriter sw = new StreamWriter(npcs);
    sw.Write(output);
    sw.Flush(); // Because using is bad. (Closes the pipe, not just the StreamWriter)

    String response = "";
    StreamReader sr = new StreamReader(npcs);
    char[] buffer = new char[1024];
    Int32 count = 0;
 
    while ((count = sr.Read(buffer, 0, 1024)) > 0)
    {
        response += new String(buffer, 0, count);
        if (count < 1024) break;
    }
 
    return response;
} 

如果命令因任何原因失败,response 将是“fail”。

当命令成功时,response 会根据发送的命令而不同;“add”、“remove”和“tag”都会返回“success”。“find”可能是最大的操作,它会返回整个匹配列表(可以轻松达到数千个)。“find_path”返回单个块的所有标签,不太可能超过756字节。“path”返回数据库的路径,预计不会超过260字节。

ASH 对每个连接的进程一次只处理一个命令(再加上 ASH 服务器进程本身一次一个)。

但我不想!

所以,假设你对附带的“main.cc”有点反感(这**绝对**会召唤出你身后的那个可怕家伙),你就得自己设置一切,从数据库开始。

// #include "ash_database.h"

try{
    db = new Ash::File(path);
    if(db != nullptr){ db->loadDataBase(); }
    else throw Ash::ae_memfail;
}catch(Ash::ash_error e){
    cout << Ash::parse_error(e) << endl << endl;
 
    try{
        db = Ash::File::createDataBase(path);
        if(db != nullptr) { db->loadDataBase(); }
    }catch(Ash::ash_error e){cout << Ash::parse_error(e) << endl;}
}catch(...){
    cout << "Unspecified error!" << endl;
    cout << "Program cannot continue; please try again." << endl << endl;
 
    return -1;
} 

第一次尝试是打开一个现有的数据库;Ash::File 不会自己创建数据库,所以如果打开失败,它会抛出一个异常(具体来说,是 Ash::ash_error::ae_iofail),在这种情况下,我们尝试在 path 处初始化一个数据库;ASH 非常天真,它简单地假设所有路径都是有效的。

一旦 Ash::File 对象被创建,我们调用 loadDataBase() 将其加载到内存中。

File 定义了以下方法

class File
{
public:
    // ...
    
    bool removeBlock(const std::string& path);
    void addBlock(const std::string& path, const std::string& tags);
    
    void flush();
    void loadDataBase();
    
    list<std::pair<Block*, int>> find(const std::string& search_str);
    Block* findBlockNumber(int bn);
    Block* findPath(const std::string& search_str);
    
    static bool isVersionCompatible(const unsigned char* version_buffer);
    static File* createDataBase(const std::string& path);
private:
    // ...
};

添加和删除 Block 有点不言自明;只要记住 addBlock() 不会检查重复项。

调用 flush() 将待处理的更改写入磁盘;File 对象在销毁时会自动执行此操作,但这样你就可以满足任何已有的微观管理心理需求。

如上文某处所述,find() 返回一个结果 list 和每个结果的命中次数,按降序排列。findBlockNumber() 返回一个指向编号为 bn 的单个 Block 的指针。findPath() 返回一个指向路径与 search_str 匹配的单个 Block 的指针(使用 std::string::operator=())。

isVersionCompatible() 在内部用于检查与待加载的 ASH 数据库的兼容性。它期望一个9字节的缓冲区大小,并且有时会导致非常、非常、非常戏剧性的蓝屏(不是真的)。

区块

Ash::File 会自己处理所有事情,前提是你没有修改任何 Block、更新标签或做任何事情。在那些疯狂、不可能的情况下,你可能真的想要修改现有数据,你就必须直接处理有问题的 Block

class Block
{
// ...
public:
    // ...
    
    int getBlockLocation() const;
    long long getTimeStamp() const;
    char* getPath() const;
    std::string getTags() const;
    
    void setPath(const char* nPath);
    void setTags(const char* nTags);
    Tag& modTags();
    
    void killBlock();
    void writeBlock(std::fstream& output, int header_sz) const;
private:
    // ...
};

setter 和 getter 的行为完全符合预期;它们设置和获取数据(惊喜!)。killBlock()Block 标记为待删除(即,当数据库被刷新时,这个特定区域会被写入零)。writeBlock()Ash::File 内部用于写入文件时,嗯,写入文件。你当然也可以用 writeBlock() 来实现你自己的邪恶目的,只要记住它是在块的偏移量处写入块,而不是在你邪恶的邪恶位置。modTags() 返回对 BlockTag 对象的引用,这样你就可以对它进行扭曲的医学实验了。

标签

Tag 看起来像这样

class Tag
{
public:
    // Boring stuff...
    
    void setTags(const char* t, long len = 0);
    void trim();

    void writeTags(char* out, long max) const;

    long getTagCount() const;
    std::string getTags() const;
    char* getTagPtr() const;
    
    int match(const Tag& t) const;
    int match(const char* t, long len=0) const;
private:
    // Too scary for the internet! (...)
};

如你所见,Tag 提供了各种无用的漂亮功能。

setTags() 实际上跳过了常规步骤,完全改变了所有标签;当仅仅追加标签变得太困难时,就用这个。trim() 会调整内部缓冲区的大小,使其没有未使用的字节,如果你要把 ASH 移植到 Z80 之类的东西上,这可能会有帮助。在实践中,裁剪缓冲区只会拖慢 ASH 的速度;然而,如果 A.) 你真的在只有1K内存的 Z80 上运行这个,或者 B.) 你有某种方法保证标签不会被频繁修改,并且需要内存来做更重要的事情,比如**《Hello Kitty 岛屿冒险,第二部:毛茸茸之灾》**,那么它可能会有帮助。writeTags() 将内部缓冲区复制到 out。我确信你自己能找到它的用处。如果找不到,就重新编译一个没有它的版本什么的。**看我关不关心**(我真的,真的很关心!别让我哭!)。getTagCount() 返回 Tag 对象中的标签数量;标签由竖线分隔;你自己算吧。你可能还会注意到 Tag 有一个(上面没写)方括号操作符引用之类的东西(operator[]()),它返回指定索引处的标签字符串。信不信由你,这两者实际上是相关的。现在,你可能会想做这样的事

/* The following is an example of a bad practice; don't do it. */

string a;

for ( long i = 0; i < getTagCount(); i++ ){
    a = my_tag[i];
    // Do stuff with "a" here
}

从技术上讲,上面这段代码可以编译。但它是错的。所以不要这样做。那是不好的。说真的。别。如果你这么做了,你身后的那个家伙可能会再次生气。这是低效的。所有那些讨厌的 C 程序员都会把你当作反面教材,用来证明为什么 C++ 那么烂,以及为什么我们应该用 C 来做所有事情。真的。(**但还是试试吧!**)

getTags()std::string 的形式返回所有内部标签,其明确目的是以一种更便于编辑的形式获取所有内部标签。例如,如果你有一个特别复杂的操作,而 Tags 目前完全不支持(参见:正则表达式),你可以这样做

/* The following is also something that will get you burned at the stake. */

string some_tags = my_tag.getTags();
some_regex_function(some_tags, some_input);
my_tag.setTags(my_tag.c_str(), my_tag.length());

getTagPtr() 直接返回一个指向 Tag 内部缓冲区的指针;所以不要对它做任何坏事。

match() 方法,顾名思义,是用来匹配标签的

Tag my_tag;
my_tag.setTags("the_ramones|awesome|punk|blitzkrieg_bop|multiple_bops");

int matches = my_tag.match("blitz");
cout << "Matches: " << matches << endl;
// Outputs "Matches: 1"

matches = my_tag.match("bop");
cout << "Matches: " << matches << endl;
// Outputs "Matches: 2"

如果你跳过了,背景部分有关于这到底是如何工作的更详细的解释(我不怪你,我也会跳过)。

Tag 还包含几个相当重要的运算符,因为没有它们,整个 ASH 可能会(可能)瘫痪

class Tag
{
public:
    // ...
    Tag& operator=(const Tag& t);
    Tag& operator=(Tag&& t);
    Tag& operator+=(const Tag& t);

    bool operator==(const Tag& t) const;
    std::string operator[](long i) const;

    // ...
private:
    // ...
};

赋值运算符实际上非常、非常、非常通用。它们的作用完全符合预期;而且可悲的是,它们甚至没有任何奇怪的行为(据我所知),如果不加以考虑就可能导致整个网络崩溃。operator+=() 有点不同,因为它对实际添加到 Tag 的标签有点挑剔(如上文某处简要提到的),因为我认为一个普通的追加运算符会很傻。基本上,它只添加唯一的标签,而不是追加**所有**标签

Tag my_tag;
my_tag.setTags("the_ramones|awesome|punk|blitzkrieg_bop|multiple_bops");

Tag my_other_tag;
my_other_tag.setTags("the_ramones|punk|blitz");

my_tag += my_other_tag;

cout << my_tag.getTags() << endl;
// Outputs "the_ramones|awesome|punk|blitzkrieg_bop|multiple_bops|blitz"

就像 ASH 的其他所有东西一样,这个运算符坚信速度比空间效率更重要,因此使用的内存比实际使用的要多(是的,这确实说得通)。添加的唯一标签越多,浪费的空间就越少。

比较运算符非常字面化,只有在两个 Tag 完全相同时才返回 true;对于更模糊的比较,我强烈推荐使用 match() 函数(不,这绝对没有偏见!)。方括号操作符(operator[]()),如上所述,返回指定索引处的标签。

Tag my_tag;
my_tag.setTags("the_ramones|awesome|punk|blitzkrieg_bop|multiple_bops");

cout << my_tag[3] << endl;
// Outputs "blitzkrieg_bop"

cout << my_tag[105] << endl;
// Crashes, because *you* didn't catch the exception!

已知问题

1) 它无法在我的 Linuxuuu 上编译!

实际上,如果你从 main.cc 中移除多线程/管道处理部分,技术上是可以编译的,但我明白你的意思,愤怒的 GNU/Linux 用户。我肯定会完全无视整个 GNU/Linux 社区,永远、永远都不会去管它最终会处理它的。(看到我刚才做了什么吗?现在充满了各种悬念。“他刚才只是在开玩笑吗?**还是他会无视我们的困境?**”)虽然我确实希望 ASH 是跨平台的,但有一些我认为非常严重(比甜饼怪和《鬼玩人2》加起来还要严重)、惊天动地的问题(尽管对用户来说是完全透明的,而且数量太多无法在此列出)需要优先处理(你永远猜不到是什么!)。显然你可以自己打补丁,但新版本的 ASH 可能会破坏与旧版本的兼容性(所以你可能最终不得不在我推出对 GNU/Linux 的*官方*支持之前,为每个新版本重新打补丁)。就目前而言,如果你为 GNU/Linux 编译 ASH,它可以做 Windows 版本能做的一切,除了连接到外部进程(所以不先修改代码就无法为它创建 GUI)。

2) 实验 #735 卡死了!

我把这主要归因于我对 .NET 的不熟悉;在两种特定情况下,实验 #735 会完全停止响应。第一种是向 ASH 数据库添加一个非常大的文件夹时;这**不是**一个 bug,我只是懒得添加进度条;在这些情况下,你只能等着它完成。它之所以如此之慢,是因为实验 #735 是一张一张地添加图片,每次都必须创建一个新的 String,发送它,等待响应,然后处理下一张。我知道这很低效,但实验 #735 的初衷更多是一个测试性的东西,而不是一个实用性的东西。第二种情况是一个实际的问题。当实验 #735 进行搜索,而 ASH 返回超过约600个结果时,管道似乎被堵塞了,实验 #735 会一直冻结直到 ASH 关闭。到目前为止,这种行为在 Windows 7 和 Vista 上,在两台不同的电脑上都是一致的,所以我猜我在这里严重搞砸了什么。我可能不会负责任地处理这个问题,而是可能会使用中间文件之类的东西来解决。

3) 实验 #735 突然戏剧性地崩溃了!

现在,我不能完全确定,但我很确定这个不是我的错。在测试过程中发现,某些 GIF 文件(它们本身是无害且功能完好的)显然会导致 GDI+ 抛出一些可怕的异常并使应用程序崩溃。既然我没有写微软的 GIF 解析代码或他们的 PictureBox 控件,我想我可以安全地怪他们。技术上,微软可能已经修复了这个问题,虽然我不知道,因为我完全忘了这是在哪个 .NET 版本上发生的(**我就是这么牛**。)

据我所知,就这些了;*如果你发现任何其他问题,请大声抱怨*。

结论

这部分是我在英语课上**总是**被扣分的地方,所以我连试都懒得试了。

玩得开心!(或者不开心。扫兴鬼。)

© . All rights reserved.