声明:本文已获作者Craig Stuntz翻译授权。
作者 | Craig Stuntz
译者 | 弯月,责编 | 郭芮

头图 | CSDN 下载自视觉中国
出品 | CSDN(ID:CSDNnews)
以下为译文:
相等的事理
我来展示一下在编程措辞中相等有多么随意马虎出错。但首先我要阐明一下相等应有的样子容貌,而这实在非常难阐明!
当我们谈论相等“该当如何”时,我们必须指出是特定语境下的相等,由于相等的成立有许多办法,许多办法在不同的语境下都是成立的。
“数学的精髓在于,同一个东西可以用不同的办法呈现给我们。”
——Barry Mazur,《When is one thing equal to some other thing》
定律
我说过,在不同的语境下,相等有不同的含义,但只管如此,有些东西是永久成立的。这便是相等的定律。
相等是一个二元操作,它是:
反射的,即对付任意值 a 都有 a = a
对称的,即a = b可以推出b = a,反之亦然
通报的,即如果a = b且b = c,则a = c
在编程的天下中,我们须要增加一条定律,由于有时候程序员会做一些奇怪的事情:
相等必须是:
同等的,即如果a = b且a或b的任何字段都没有发生变革,那么稍后再次检讨时a = b应该依然成立。
上面的定律看似大略,但盛行的编程措辞乃至连如此大略的定律都无法遵守。但更严重的关于相等的问题乃至都很难精确描述。
构造相等性
在编程措辞对付相等的各种实现中,一个主要的差异便是构造相等和引用相等。
构造相等会检讨两个引用是否为同一个值。F#默认采取了却构相等:
type MyString = { SomeField : string }
let a = { SomeField = \"大众Some value\"大众 }
let b = { SomeField = \"大众Some value\"大众 }
if a = b then // 返回true, 进入 \公众then\"大众 代码块
但在C#中则不是这样,C#利用的是引用相等。引用相等哀求两个被比较的工具是同一个工具。换句话说,它会比较两个变量是否指向同一个内存地址。指向两个不同内存地址的引用会被判为不相等,纵然它们的值完备一样:
class MyString {
private readonly string someField;
public string SomeField { get; }
public MyString(string someField) => this.someField = someField;
}
var a = new MyString(\"大众Some value\"大众);
var b = new MyString(\"大众Some value\公众);
if (a == b) { // 返回 false, 不会进入代码块
其他措辞会让你选择。例如,Scheme供应了equal?来检讨构造相等,eq?来检讨引用相等。Kotlin供应了==用于构造相等,===用于引用相等(不要与JavaScript的==和===操作符稠浊,JavaScript的那个是完备不同的东西)。
程序中何时应该利用构造相等?如果变量的值不会改变更,那么险些任何情形下都该当利用构造相等!
我所知的绝大多数编程措辞在诸如integers之类的类型上都利用构造比较。除了Java之外,int类型进行构造比较,Integer类型进行引用比较,这迷惑了一代又一代的程序员。Python的is也有类似的问题。
对付引用类型(如工具)也应该进行构造比较。考虑一个单元测试,你希望检讨返回的工具是否即是你期待的值。在利用构造相等的措辞中,这个操作非常大略:
[<TestMethod>]
let ``The result of the calculation is the expected value`` =
let expected = { SomeField = \公众Some value\公众; SomeOtherField = 15; StillAnotherField = true; ... }
let actual = calculate
Assert.AreEqual(expected, actual)
但如果措辞不支持构造相等,而开拓者须要自行开拓,就会碰着难题。
引用相等
但正如我刚才说过的那样,某些特定情形下不应该利用构造相等。一种情形便是措辞支持变量内容改变的情形,而绝大多数编程措辞都支持。当某个变量的值被改变后,说这个变量即是另一个变量显然是不合理的。当然,你可以说在进行比较的时候,这两个变量(在构造上)是相等的,比如在单元测试的末了一行时是相等的,但一样平常情形下你无法假设这两个变量是同一个东西。这点理解起来有些困难,我来举例解释。
我们假设有一个工具,表示一个人。在采取了却构相等的F#中,我可以这样写:
type Person = { Name : string; Age : integer; Offspring : Person list }
现在我有两个朋友Jane和Sue,她们都有一个叫John的儿子,年事都是15岁。他们是不同的人,但姓名和年事都一样。没问题!
let jane = { Name = \公众Jane\"大众; Age = 47; Offspring = [ { Name = \"大众John\"大众; Age = 15; Offspring = [] } ] }
let sue = { Name = \"大众Sue\"大众; Age = 35; Offspring = [ { Name = \"大众John\"大众; Age = 15; Offspring = [] } ] }
也可以这样写:
let john = { Name = \"大众John\"大众; Age = 15; Offspring = };
let jane = { Name = \"大众Jane\公众; Age = 47; Offspring = [ john ] }
let sue = { Name = \"大众Sue\"大众; Age = 35; Offspring = [ john ] }
这两段代码的功能完备一样。我没办法差异两个儿子,纵然我知道他们是不同的人。但这没有问题!
如果我须要差异他们,我可以把他们DNA的hash之类的属性加到Person类型中。但如果我只须要知道他们的名字和年事,那么是否能区分两个工具并不主要,由于不管怎么区分,它们的值都是一样的。
假设Jane的儿子改名成Pat。F#不支持改变变量的值,以是我须要为John(还有Jane!
)创建新的Person实例:
let newJane = { Name = \"大众Jane\"大众; Age = 47; Offspring = [ { Name = \"大众Pat\公众; Age = 15; Offspring = [] } ] }
这个新的变量newJane彷佛有点奇怪,但实际上并不会构成问题。上面的代码没有问题。现在用C#试一下,在C#中,变量默认情形下是可以修正的:
var john = new Person(\"大众John\"大众, 15, );
var jane = new Person(\公众Jane\公众, 15, new List<Person> { john });
var sue = new Person(\"大众Sue\"大众, 15, new List<Person> { john });
这段代码显然是禁绝确的:如果Jane的儿子改名为Pat,我可以直接改变引用的值:
jane.Offspring.First.Name = \公众Pat\"大众;
但我就会创造Sue的儿子也改名了!
因此,纵然两个儿子最初的名字是一样的,但他们并不相等!
以是我该当写成:
var jane = new Person(\"大众Jane\"大众, 15, new List<Person> { new Person(\"大众John\"大众, 15, ) });
var sue = new Person(\"大众Sue\"大众, 15, new List<Person> { new Person(\"大众John\"大众, 15, ) });
这样Jane和Sue的孩子便是引用不相等。以是,在可以改变变量内容的措辞中,默认采取引用相等是合理的。
另一种该当采取引用相等的情形是,事先知道引用相等的结果与构造相等相同。测试构造相等显然须要额外开销,如果真的须要测试构造相等,那么这个额外开销是正常的。但是,假设你创建了大量的工具,而且事先知道每个工具都是构造不相等的,那么花费额外开销来测试构造相等是没有必要的,由于仅仅测试引用相等就可以得出同样的结果。
相等性的表示
在实数中,0.999……(无限循环小数)即是1。把稳这里说的“实数”与编程措辞中的Real类型不一样。在数学中,实数是无限的,而编程措辞中的实数是有限的。因此,编程措辞中没有0.999……这样的写法,但没紧要,你可以利用1,反正两者的值是一样的。
这实质上是数学家在表示实数系统时采取的一种选择。如果在系统中加入其余一种工具,比如无限小的数,那么0.999……和1就不相等了。
“但是这并不即是说规范可以任意确定,由于不接管一种规范,一定会导致不得不发明奇怪的新工具,或者不得不放弃某些熟知的数学规则。”
——Timothy Gowers,《Mathmetics: A Very Short Introduction》
类似地,在实数系统中,1/2和2/4表示同样的值。
不要把这些“相等”与JavaScript或PHP中的“不严格”相等运算符==稠浊。这些相等跟那些运算符不一样,这些相等依然遵照相等的定律。主要的是要认识到,工具的相等可以用不同的办法来表达。
在IEEE-754浮点数系统中,-0 = 0。
内涵和外延
一个函数何时即是另一个函数?绝大多数编程措辞会进行引用相等的比较,我以为这没有问题。由于,对函数进行构造比较有什么意义呢?大概我们可以利用反射来检讨函数的实现,看看它们实现是否一样?但若何才叫“一样”?变量名是否必须完备一样?快速排序和归并排序是不是“一样”的函数?
因此我们说,只要函数对付同样的输入返回同样的输出(不管其内部实现如何),函数便是外延相等的,而如果内部定义也一样,则是内涵相等的。当然,这也取决于语境。可能在某个语境中,我须要常数韶光的函数,在另一个语境中,速率无关紧要。主要的是,必须有语境才能定义相等,才能用它来比较两个函数。
我不知道是否有哪种措辞在比较函数时考试测验过采取引用相等之外的方法。但很随意马虎想出,这会很有用!
(例如,优化器考试测验移除重复的代码等。)你只能自己实现,但我不得不说,没有相等比较,总要比缺点的相等比较强。
相等和赋值
当程序员的第一天就学过,“即是”这个名字有两种不同的观点。一种是赋值,另一种是测试相等性。在JavaScript中须要这样写:
const aValue = someFunction; // 赋值
if (aValue === 3) { // 测试相等
这两者实质上是不同的。比较返回布尔值,而在面向表达式的措辞(如Ruby)中,赋值返回被赋的值。
以是Ruby代码可以这样写:
a = b = c = 3
实际上会把3赋给变量a,b和c。不要在引用类型上考试测验,很可能得不到你想要的结果!
在非面向表达式的措辞(如C#)中,赋值没有返回值。
在数学中,赋值和测试相等性都利用相等运算符:
if aValue = 3 ...
where aValue = someFunction
(而且在数学中,有时候=还用于其他关系,如条约(congruence)。与数学中的其他东西一样,这里也要区分语境;在阅读论文或书本时必须把稳语境。)
为什么数学不哀求两种不同的操作,而编程措辞哀求?由于在数学中可以轻易判断出语境,而且也并非所有措辞都哀求不同的运算符。例如,F#中赋值和测试相等都采取=。只管两者采取相同的符号,但赋值和测试相等是完备不同的操作。
let aValue = someFunction; // 赋值
if aValue = 3 then // 测试相等
语法的选择部分出于历史缘故原由:F#基于ML,而ML基于数学;而JavaScript的语法基于Java→C→Algo→FORTRAN。
用于编译FORTRAN代码的机器很难根据语法来区分两种情形,因此采取不同的运算符是合理的。于是C措辞把这个“特性”带到了新的高度,以是你乃至可以写:
int aValue = someFunction; // 赋值
if (aValue = 3) { // 也是赋值!
给没有C措辞履历的人阐明一下:这段代码先用3覆盖aValue,然后由于表达式aValue = 3的值为3,因此if的条件为真,因此会连续实行if块内的代码。常日这种写法都是缺点的,因此许多C程序员会将if块的条件反过来写,来避免造成该缺点:
int aValue = someFunction; // 赋值
if (3 == aValue) { // 测试相等
// [...]
if (3 = aValue) { // 语法缺点:无法将 aValue 赋值给 3.
相等性的利用缺点
通过上面的解释,希望大家都已经明白相等性并不大略,“精确”的实现取决于语境。只管如此,编程措辞常常会把最随意马虎的地方搞错!
很多时候,这是相等性与其他措辞特性的组合造成的,如隐式类型转换。
常见缺点:相等性不是反射的
回顾一下相等性的反射率,即任何值都即是它自身,a = a。
在.NET中,如果在值类型上调用Object.ReferenceEquals,其参数会在实行方法之前分别进行打包,因此纵然通报同一个实例,也会返回假:
(来自文档的例子)
int int1 = 3;
Console.WriteLine(Object.ReferenceEquals(int1, int1)); // 输出 False
这意味着在任何.NET措辞中 a = a 都不一定为真,因此不知足反射率。
在SQL中,不即是自身,因此表达式 = (或者更可能的情形是,SOME_EXPRESSION = SOME_OTHER_EXPRESSION时两者都可能为)会返回false。这会导致下面乱糟糟的语句:
WHERE (SOME_EXPRESSION = SOME_OTHER_EXPRESSION)
OR (SOME_EXPRESSION IS AND SOME_OTHER_EXPRESSION IS )
而更可能发生的情形是,开拓者会忘却的分外规则从而导致bug。一些数据库做事器的SQL措辞支持IS NOT DISTINCT FROM,它的功能才是=该当有的功能。(或者我该当说,它没有不做=该当做的事情?)否则,就必须利用上面例子中的SQL语句。最好的办理办法便是尽可能利用不许可的列。
IEEE-754浮点数也有同样的问题,即NaN != NaN。一种阐明是,NaN表示某个不愿定的“非数字”结果,而不同打算得出的NaN并不一定是同一个不愿定的非数字,以是这个比较本身便是禁绝确的。例如,square_root(-2)和infinity/infinity两者的结果都是NaN,但显然它们不一样!
有时候SQL的问题也可以类似地阐明。这样造成的问题之一便是术语的含义过多:NaN和表示的是“未知”,还是“禁绝确的值”,或者是“短缺值”?
对付此类正常的浮点运算中不会涌现的问题,办理方法之一便是采取联合(union)类型。在F#中可以这样写:
type MaybeFloat =
| Float of float
| Imaginary of real: float imaginary: float
| Indeterminate
| /// ...
然后就可以在打算中精确处理这些情形了。如果在打算中碰着预见之外的NaN,可以利用signaling NaN来抛出非常。
Rust供应了Eq和PartialEq两个trait。没有实现Eq,是==运算符不屈服反射率的一个旗子暗记,而Rust中的浮点类型就没有实现Eq。但纵然不实现Eq,你依然可以在代码中利用==。实现Eq可以将工具作为hash map的键利用,可能会导致其他地方的行为发生变革。
但是=和浮点数还有更严重的问题。
常见缺点:相等过于精确
我想许多开拓者都熟习IEEE-754浮点数的比较问题,由于绝大多数措辞的“float”或“double”的实现都是IEEE-754。10 (0.1) 不即是1,由于“0.1”实际上即是0.100000001490116119384765625,或0.1000000000000000055511151231257827021181583404541015625。如果你对此问题感到陌生,你可以阅读这篇文章(https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/),但这里的重点是,在浮点数上利用==进行比较是完备不屈安的!
你必须决定哪些数字是主要的,然后据此进行比较。
(更糟糕的是,浮点数是许多其他类型的根本,如某些措辞中的TDateTime类型,以是纵然一些相等比较本该合理的地方,也不能正常事情。)
比较浮点数的精确方法是看它们是否“附近”,而“附近”在不同语境下有不同的含义。这并不是大略的==能够完成的。如果你创造常常须要做这种事情,那么大概你该考虑利用其他数据类型,如固定精度的小数。
既然如此,为什么编程措辞要在无法支持的类型上供应==比较呢?实在编程措辞为每一种类型都供应了==,程序员须要依赖自己的知识来判断哪些不能用。
SML的实现解释(http://sml-family.org/Basis/real.html)上这样说:
判断real是否为相等的类型,如果是,那么相等本身的意义也是有问题的。IEEE指出,零的符号在比较中应该被忽略,而任意一个参数为NaN时,相等比较应该返回false。这些约束对付SML程序员来说非常麻烦。前者意味着 0 = ~0 为true,而r/0 = r/~0为false。后者意味着r = r可能涌现返回false的非常情形,或者对付ref cell rr,可能存在 rr = rr 成立但是 !rr = !rr 不成立的情形。我们可以接管零的无符号比较,但是认为相等的反射率、构造相等,以及<>和not o =的等价性应该被保留。这些额外的繁芜性让我们作出决定,real不是具有相等性的类型。
通过禁止real拥有=运算,SML强制开拓者思考他们真正须要什么样的比较。我认为这个特性非常好!
F#供应了[<NoEquality>]属性,来标记那些=不应该被利用的自定义类型。遗憾的是,他们并没有将float做上标记!
常见缺点:不相等的“相等”
PHP有两个单独的运算符:==和===。==的文档将其称为“相等”,并记载到“如果在类型转换后$a即是$b则返回TRUE”。不幸的是,这意味着==运算符是不可靠的:
<?php
var_dump(\公众608E-4234\"大众 == \"大众272E-3063\公众); // true
?>
只管这里比较的是字符串,但PHP创造两者都可以被转换为数字,以是就进行了转换。由于这两个数字非常小(例如第一个数字是608 10^-4234),而我们之前说过,浮点数比较非常困难。将这两者都转换成浮点数float(0)将导致它们被四舍五入成同一个值,因此该比较返回真。
把稳这与JavaScript的行为不同。JavaScript也有与PHP类似的(但并不是一样的!
)==和===运算符;但JavaScript会认为两侧都为字符串,然后返回比较结果false。
幸运的是,PHP供应了===(“全等”)运算符,在这种情形下能给出精确结果。我想说永久不要利用==,但==会在工具上实行构造比较,有时候正是你须要的!
因此我只能说,利用==时要格外小心,由于它不能在根本类型上精确事情。
常见缺点:相等不是对称的
如果你要在Java中重载.equals,那么你必须卖力确保相等的定律成立!
如果不加把稳,那么很随意马虎就会导致不对称的相等,即a.equals(b) != b.equals(a)。
纵然不考虑的情形(由于会导致PointerException,而.equals()是许可这种情形发生的:https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#equals-java.lang.Object-),如果你继续一个类并重载.equals,也最好多加小心!
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == )
return false;
if (!o.getClass.isAssignableFrom(getClass)) // 危险!
这一步是错的! return false;
ThisClass thisClass = (ThisClass) o;
// 字段比较
// ...
}
如果ThisClass和ASubtypeOfThisClass都用类似上面的代码重载了.equals,那么a.equals(b)就可能不即是b.equals(a)!
精确的比较该当是:
if (getClass != o.getClass)
return false;
这不仅仅是我的个人意见,也是Object.equals的左券的哀求(https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#equals-java.lang.Object-)。
常见缺点:相等没有通报性
回顾一下相等比较的定律之一便是应该具有通报性,即如果a = b 且 b = c,那么 a = c。不幸的是,与类型转换(type coersion)放在一起后,许多措辞都会在这里出问题。
在JavaScript中,
'' == 0; // true
0 == '0'; // true
'' == '0'; // false!
因此在JavaScript中永久不要利用==,该当利用===。
常见缺点:相等性不一致
在Kotlin中,==会根据变量类型返回不同的值,纵然对付同一个变量:
fun equalsFloat(a: Float, b: Float) {
println(a == b);
}
fun equalsAny(a: Any, b: Any) {
println(a == b);
}
fun main(args: Array<String>) {
val a = Float.NaN;
val b = Float.NaN;
equalsFloat(a, b);
equalsAny(a, b);
}
// prints false, true
这是一个非常不幸的措辞特性组合,可能会导致违反直觉的行为。
常见缺点:在应该利用构造相等的地方利用引用相等
考虑如下用C#编写的MSTest单元测试:
[TestMethod]
public void Calculation_Is_Correct {
var expected = new Result(SOME_EXPECTED_VALUE);
var actual = _service.DoCalculation(SOME_INPUT);
Assert.AreEqual(expected, actual);
}
这段代码能正常事情吗?我们不知道!
Assert.AreEqual终极会调用Object.Equals,默认会进行引用比较。除非你重载了Result.Equals进行构造比较,否则这个单元测试无法正常事情。Object.Equals认为,如果类型是可改变的,那么不应该重载。常日来说这是合理的,但在单元测试中却未必。(这是由于.Equals()本应比较.GetHashCode(),而一个工具的hash code在工具的生命周期中该当不发生改变。).NET framework中对付引用类型的最靠近“有担保的构造比较”的是IEquatable<T>,但Assert.AreEqual并没有利用,纵然实现了也不会利用。
而NUnit的情形更糟(https://github.com/nunit/nunit/issues/1249)。
(相反,Java的Object.hashCode在工具的字段发生变革时是许可变革的。https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#hashCode())
该当若何看待相等
没想到关于=运算符我写了这么多还没写完!
好吧,这已经远远超过了运算符本身。为什么如此繁芜?基本上有两个缘故原由:
非实质的繁芜性:我们的编程措辞在相等比较方面做得并不好。常常完备不能正常事情,乃至在不能正常事情时也不会明确表示这一点,例如会在本应进行引用比较的地方利用构造比较。
实质的繁芜性:相等性本身便是繁芜的,如比较浮点数。而在诸如比较函数等边缘情形下就更为繁芜。
另一种划分方法便是“该当由编程措辞的实现者卖力办理的问题”(上面的“非实质的繁芜性”)和“该当由编程措辞的利用者卖力办理的问题”。
编程措辞该当怎么做?
关于非实质的繁芜性,现状是险些每一种主流编程措辞对付相等性的实现都有问题。这个“必须遵照几个定律的大略运算”正是编程措辞为了担保精确性而依赖的东西!
但在我看来,只有SML真正思考了若何在语义和运行时/标准库方面同时担保符合定律的相等性,而SML完备不是主流措辞。
首先,在禁止相等比较的地方,编程措辞该当很随意马虎创建不许可相等比较的类型,由于这易操作完备没有必要繁芜(如F#中的[<NoEquality>]),然后该当在标准库中尽可能多地利用该特性,如浮点类型。
编程措辞必须非常明确地指出构造相等和引用相等之间的差异。永久都不应该存在行为不愿定的情形。绝大多数编程措辞会重载==,根据引用的类型(多数情形是根据值或引用的差异),用它来表示构造相等或引用相等,这样做一定会让开发者感到困惑。
Kotlin已经非常靠近精确了,它的===表示引用相等,==表示构造相等,只管出于某些缘故原由,对付值类型它会将===看做==,而不是引发编译缺点。目标该当是减少开拓者的困惑。它希望让开发者明白,===表示“引用相等”,而不是等号越多越好。
我不知道还有哪些许可改变变量值的措辞能够用不困惑的办法处理构造相等的。但很随意马虎想象空想状态该当若何!
准备两个运算符,一个表示构造相等,一个表示引用相称,只在编程措辞可以合理地支持的语境下才许可相应的运算符。例如,如果.NET的Object.ReferenceEquals和值类型不进行包裹,并且利用类似于IEquatable<T>的东西许可成功够许愿利用构造相等运算符,那么开拓者就很随意马虎弄清楚哪个是哪个。
程序员该当怎么做?
大概你读了这篇文章后会以为,“哇,相等好繁芜!
我还是不要编程了,回家种地算了。”但这篇文章如此之长的缘故原由紧张是太多的措辞都做错了。都为难刁难的确须要些心思,但并不是太难。肯定比种地要大略。
在已有的类型上进行相等比较时,先问问自己:
在这里进行相等比较本身合理吗?
如果合理,那么是该当进行构造比较,还是引用比较?
对付相应的比较方法,我采取的编程措辞供应了哪些支持?
我采取的编程措辞对付该比较方法的实现是精确的吗?
在设计自定义类型时也可以讯问类似的问题:
我的类型该当支持相等比较吗?还是须要一个更繁芜的比较,就像float那样?
我的类型该当是可改变的吗?它会对相等性产生若何的影响?
该当支持引用比较?还是构造比较?还是该当同时支持两者?
如果你的类型是可改变的,则该当考虑将其改成不可改变的。纵然措辞默认是可改变的,这一点也可以实现!
这样做除了能在相等性比较方面得到许多好处之外,不可改变的架构还有许多其他的好处。采取了不可改变数据构造的C# Roslyn编译器就是非常好的例子:
语法树的第三个属性是,它们是不可改变的,而且是线程安全的。这意味着,在得到一棵树之后,它便是当前代码状态的快照,而且永久不会改变。这样多个用户可以在不同的线程中与同一个语法树同时进行操作,而无需担心去世锁或重复的问题。由于树是不可改变的,也不能针对树进行直接的改变,因此卖力创建和修正语法树的工厂方法实际上会创建树的新快照。树本身的效率很高,由于它会重用底层结点,以是创建新版本的速率很快,只须要利用少量内存。
——.NET Compiler Platform SDK文档
原文:https://www.craigstuntz.com/posts/2020-03-09-equality-is-hard.html
☞当代编程措辞大 PK,2020 年开拓者关心的七大编程措辞!
☞MySQL 狠甩 Oracle 稳居 Top1,私有云最受重用,大数据人才匮乏!
| 中国大数据运用年度报告
☞如何用CNN玩转AlphaGo版的五子棋?
☞曾经摸鱼的程序员,如今在武汉志愿加班
☞区块链和大数据一起能否开启数据完全性的新纪元?
☞以太坊2.0、分片、DAG、链下状态通道……概述区块链可扩展性的办理方案!