大家好,欢迎来到IT知识分享网。
1.发展进程
开发过程是bug和缺陷开始的地方。在发布之前,利用帮助您避免或发现这些问题的工具:
编码标准
标准的一致使用可以导致更易于维护的代码,特别是在由多个开发人员或团队编写和维护的代码库中。FxCop、StyleCop和ReSharper等工具通常用于执行编码标准。
开发人员:在抑制违规行为和分析结果之前,要仔细考虑它们。它们识别代码路径中的问题,这些问题不像您预期的那么不寻常。
代码评审
代码评审和对编程是开发人员通过仔细检查他人编写的源代码来执行任务的常见做法。其他人希望认识到作者所犯的错误,如编码或实现错误。
代码评审是一种很有价值的实践,但是依赖于人的本质是错误的,而且很难扩展。
静态分析
静态分析工具在不运行代码的情况下分析代码,查找违反编码标准或存在缺陷等问题,而无需编写测试用例。它在发现问题方面是有效的,但是您需要选择一些工具来识别有价值的问题,而不会出现太多的错误。C#静态分析工具包括Coverity、CAT.NET和VisualStudio代码分析。
动态分析
动态分析工具在代码运行时分析代码,帮助您查找安全漏洞、性能和并发问题等缺陷。它在运行时环境中分析代码,因此它的有效性受到测试工作量的限制。VisualStudio提供了许多动态分析工具,包括并发可视化器、智能跟踪和分析工具。
经理/团队领导:利用开发最佳实践来避免常见的陷阱。仔细考虑可用的工具,以确保它们与您的需要和文化兼容。让您的团队保持诊断噪音水平的可管理性。
测试
有许多类型的测试,例如:单元测试、系统集成测试、性能测试、渗透测试。在开发阶段,大多数测试由开发人员或测试人员编写,以验证应用程序是否满足其要求。
只有当测试执行正确的代码时,测试才是有效的。在实现功能和测试的同时保持开发速度可能是一项挑战。
发展最佳做法
投入时间来识别和配置工具,找出你关心的问题,而不为开发人员创造额外的工作。频繁和自动地运行分析工具和测试,以确保开发人员在代码还未被记住时解决问题。
尽快处理所有诊断输出–无论是编译器警告、标准冲突、通过静态分析识别的缺陷还是测试失败。如果有趣的新诊断在“不关心”或忽略诊断的海洋中丢失,那么检查结果的工作将会增加,直到开发人员不再费心为止。
采用这些最佳实践有助于提高代码的质量、安全性和可维护性,以及提高开发人员的一致性和生产力以及版本的可预测性。
2.Gotchas型
C#的主要优点之一是其灵活的类型系统;类型安全有助于及早发现错误。通过执行严格的类型规则,编译器能够帮助您维护正常的编码实践。C#语言和.NET框架提供了丰富的类型集合,以满足最常见的需求。大多数开发人员对常见类型及其使用有很好的理解,但也存在一些常见的误解和误用。
有关.NET Framework类库的更多信息可以在MSDN库中找到。
理解并使用标准接口
某些接口与惯用的C#特性有关。例如,IDisposable允许使用常见的资源处理习惯用法,例如“使用”关键字。理解何时使用接口将使您能够编写易于维护的惯用C#代码。
避免ICloneable-设计者从未明确指出克隆对象是打算是深拷贝还是浅拷贝。由于克隆对象的正确行为没有标准,因此没有能力将接口作为契约进行有意义的使用。
结构
尽量避免写到结构中。将它们视为不可变会防止混淆,并且在共享内存场景(如多线程应用程序)中更为安全。相反,如果需要更改值,则在创建结构和创建新实例时使用初始化器。
了解哪些标准类型/方法是不可变的,并返回新值(例如,String、datetime)和可变的值(清单.EnDigator)。
弦
字符串可能为空,因此在适当的时候使用方便的函数。计算(s.Length==0)可能抛出一个NullReferenceException,而String.IsNullOrempty(S)和String.NullOrWhitesspace(S)则优雅地处理NULL。
标记枚举
枚举类型和常量值通过将魔术数字替换为公开值含义的标识符,有助于提高代码的可读性。
如果发现需要创建枚举集合,则标记枚举可能是更简单的选择:
[Flag]
public enum Tag {
None =0x0,
Tip =0x1,
Example=0x2
}
这使您可以轻松地为一个片段拥有多个标记:
snippet.Tag = Tag.Tip | Tag.Example
这可以改进数据封装,因为您不必担心通过标记属性getter公开内部集合。
平等比较
有两种类型的平等:
引用相等,这意味着两个引用引用同一个对象。
值相等,这意味着两个引用上不同的对象应该被认为是相等的。
此外,C#提供了多种方法来测试等式。最常用的技术是:
=和!=运算符
从对象继承的虚拟相等方法
静态对象方法
Iequable<T>接口的等号方法
静态对象.引用等式方法
很难知道引用或价值相等是有意的。检查msdn相等主题,以确保您的比较工作正常:http://msdn.microsoft.com/en-us/library/dd183752.aspx
如果覆盖等于,不要忘记IEquable<T>、GetHashCode()等等,如MSDN中所述。
注意未输入的容器对超载的影响。考虑比较“myArrayList[0]=myString”。数组列表元素的编译时类型为“Object”,因此使用引用相等。C#编译器会警告您这个潜在的错误,但是在许多类似的情况下,编译器不会对意外的引用相等发出警告。
3.Gotchas类
封装数据
类负责正确地管理数据。出于性能原因,他们常常缓存部分结果,或者以其他方式对其内部数据的一致性作出假设。公开访问数据会损害您缓存或假设的能力–可能会对性能、安全性和并发性产生影响。例如,公开可变成员,如泛型集合和数组,允许用户在不知情的情况下修改这些结构。
特性
属性使您能够精确地控制用户如何与对象交互,而不是通过访问修饰符来控制。具体来说,属性使您能够控制在读和写时发生的事情。
属性使您能够在重写getter和setter中的数据访问逻辑时建立稳定的API,或者提供数据绑定源。
不要从属性getter抛出异常,并避免修改对象状态。这样的愿望意味着需要一种方法,而不是属性获取器。
有关属性的详细信息,请参阅MSDN的属性设计主题:
Http://msdn.microsoft.com/en-us/library/ms229006(v=vs.120).aspx
小心有副作用的吸痰剂。开发人员习惯于相信成员访问是一项微不足道的操作,因此他们常常忘记在代码评审期间考虑副作用。
对象初始化器
可以在创建表达式本身内设置新创建对象的属性。若要为foo和Bar属性创建具有指定值的类C的新对象,请执行以下操作:
new C {Foo=blah, Bar=blam}
还可以使用特定的属性名创建匿名类型的实例:
var myAwesomeObject = new {Name=”Foo”, Size=10};
初始化器在构造函数体运行之前执行,确保字段在进入构造函数之前被初始化。由于构造函数尚未运行,字段初始化程序可能不会以任何方式引用“此”。
过度指定输入参数
为了防止专门方法的扩散,尝试使用该方法所需的最不特定的类型。例如,考虑一个迭代列表<Bar>的方法:
public void Foo(List<Bar> bars)
{
foreach(var b in bars)
{
// do something with the bar…
}
}
这段代码对于其他IEnumerable<Bar>集合应该工作得很好,但是通过为参数指定List<Bar>,您需要集合是一个列表。为该参数选择最小的特定类型(IEnumerable<T>、IC管束<T>等),以确保该方法的最大有用性。
4.仿制药
泛型是定义与类型无关的结构和算法的强大方法,可以执行类型安全。
使用清单<T>之类的泛型集合,而不是像ArrayList这样的非类型化集合来提高类型安全性和性能。
在实现泛型类型时,可以使用“Default”关键字来获取默认值无法硬编码到实现中的类型的默认值。具体来说,数值类型的默认值为0;引用和可空值类型的默认值为空。
T t = default(T);
5.转换和铸造
转换有两种类型。开发人员必须调用显式转换,编译器基于上下文应用隐式转换。
常数0可以隐式转换为枚举。当您试图调用一个使用数字的方法时,您可能最终会调用一个使用枚举的方法。
铸造描述
树树=(树)轴;当您期望obj只属于Tree类型时,请使用此选项。如果obj不是一棵树,则将引发一个残废死亡异常。
乔木=树形树;当您预期obj可能是或可能不是一棵树时,请使用此方法。如果obj不是树,则将向tree分配空值。始终遵循带有条件逻辑的“as”强制转换,以正确处理返回NULL的情况。只在必要时使用这种类型的转换,因为它需要有条件地处理返回值。这些额外的代码为更多的bug创造了机会,并使代码更难阅读和调试。
铸造通常意味着两件事之一:
您知道,表达式的运行时类型将比编译器所能推断的更具体。强制转换指示编译器将表达式视为更特定的类型。如果假设不正确,编译器将生成抛出异常的代码。例如,从对象到字符串的转换。
您知道,有一个与表达式值关联的完全不同类型的值。强制转换指示编译器生成生成此关联值的代码,如果没有异常,则抛出异常。例如,从双整数到整数的转换。
这两种类型都是危险信号。第一种类型的转换提出了一个问题:“为什么开发人员知道编译器不知道的东西呢?”如果您处于这种情况下,请尝试更改程序,以便编译器能够成功地推断出正确的类型。如果您认为对象的运行时类型可能是比编译时类型更特定的类型,那么您可以使用“is”或“as”操作符。
第二种类型的强制转换提出了一个问题:“为什么不首先在目标数据类型中完成操作?”如果您需要int类型的结果,那么使用int可能比使用Double更有意义。
关于更多的想法,见:
Http://blogs.msdn.com/b/ericlippert/archive/tags/cast+operator/
在显式转换是正确的情况下,使用适当的操作符可以提高可读性、调试能力和可测试性。
6.例外情况
例外不是条件
异常通常不应用于控制程序流;它们代表运行时可能无法恢复的意外情况。如果您预期到您应该处理的情况,请主动检查环境,而不是等待异常触发。
若要优雅地将格式不可靠的字符串转换为数字,请使用TryParse()方法;它将返回指示解析是否成功的布尔值,而不是抛出异常。
使用异常处理范围的注意事项
在CATCH内部编写代码,最后仔细阻止。控件可能由于意外异常而进入这些块;您预期已经执行的代码可能被异常跳过。例如:
Frobber originalFrobber = null;
try {
originalFrobber = this.GetCurrentFrobber();
this.UseTemporaryFrobber();
this.frobSomeBlobs();
}
finally {
this.ResetFrobber(originalFrobber);
}
如果GetCurrentFrobber()抛出一个异常,那么当执行Final块时,原始Frobber仍然是空的;如果GetCurrentFrobber不能抛出,那么为什么它在try块中呢?
谨慎处理异常
只捕获您准备处理的特定异常,并且只捕获您期望出现的特定代码部分。避免处理根类异常的所有异常或实例,除非您只打算记录并重新抛出异常。某些例外可能会使应用程序处于一种状态,即在没有进一步损坏的情况下崩溃,而不是徒劳地试图恢复和造成损害。你试图恢复元气可能会在不经意间使事情变得更糟。
在处理致命异常方面存在一些细微差别,特别是在执行最终块如何影响异常安全和调试器方面。有关更多信息,请参见:
Http://incrediblejourneysintotheknown.blogspot.com/2009/02/fatal-exceptions-and-why-vbnet-has.html
使用顶级异常处理程序安全地处理意外情况,并公开信息以帮助调试问题。谨慎地使用CATCH块来处理可以安全处理的特定案例,并将意外情况留给顶级处理程序处理。
如果你发现了一个异常,那就做点什么吧。吞咽异常只会使问题更难识别和调试。
在自定义异常中包装异常对于公开公共API的代码尤其有用。异常是方法可见接口的一部分,应该与参数和返回值一起控制异常。传播许多异常的方法很难集成到健壮、可维护的解决方案中。
抛出和重新抛出异常
当您希望在更高级别处理捕获的异常时,维护原始异常状态和堆栈可能是很好的调试辅助。仔细平衡调试和安全考虑。
好的选择包括简单地重新抛出异常:
投掷;
或在新抛出中使用异常作为InnerException:
抛出新的CustomException(…)(前);
不要像这样显式地重新抛出捕获的异常:
扔e;
这将将异常状态重置为当前行并阻止调试。
有些异常起源于代码上下文之外。您可能需要为ThreadException或UnhandledException等事件添加处理程序,而不是使用CATCH块。例如,Windows窗体异常是在窗体处理程序线程的上下文中引发的。
原子性(数据完整性)
异常不能影响数据模型的完整性。您需要确保您的对象处于一致状态–类实现所作的任何假设都不会被违反。否则,通过“恢复”,您可能只会使代码变得混乱,并在以后造成进一步的损害。
考虑按顺序修改多个私有字段的方法。如果在此修改序列的中间抛出异常,则对象可能处于无效状态。在实际更新字段之前,尝试计算出新的值,这样您就可以以异常安全的方式安全地更新所有字段。
某些类型的值(包括bool、32位或更小的数值类型和引用)的赋值保证是原子的。对于更大的类型,如双、长和十进制,没有这样的保证。在修改多个线程共享的变量时,请考虑始终使用锁语句。
7.活动
事件和委托一起工作,为类提供了一种在发生有趣事情时通知用户的方法。事件的值是在事件发生时应该调用的委托。事件类似于委托类型的字段;它们在创建对象时自动初始化为NULL。
事件就像一个值为“多播”委托的字段。也就是说,可以依次调用其他委托的委托。可以向事件分配委托;可以通过+=和-=之类的运算符操作事件。
小心种族条件
如果某个事件在线程之间共享,则有可能在您检查NULL之后并在调用它之前,另一个线程将删除所有订阅者–抛出NullReferenceException。
标准的解决方案是创建事件的本地副本,用于测试和调用。您仍然需要小心,在其他线程中删除的任何订阅服务器在意外调用其委托时都将正确操作。您还可以实现锁定,以避免出现问题的方式对操作进行排序。
public event EventHandler SomethingHappened;
private void OnSomethingHappened()
{
// The event is null until somebody hooks up to it
// Create our own copy of the event to protect against another thread removing our subscribers
EventHandler handler = SomethingHappened;
if (handler != null)
handler(this,new EventArgs());
}
有关事件和种族的更多信息,请参见:Http://blogs.msdn.com/b/ericlippert/archive/2009/04/29/events-and-races.aspx
别忘了解开事件处理程序
向事件源订阅事件处理程序将创建从源对象到处理程序的接收方对象的引用,这可以防止接收方的垃圾收集。
正确地取消挂钩处理程序可以确保既不会浪费时间调用不再工作的委托,也不会浪费内存存储无用的委托和未引用的对象。
8.属性
属性为程序集、类和属性的元数据注入有关其属性的信息提供了一种方法。它们通常用于通过反射向代码的使用者提供信息,比如调试器、测试框架和应用程序。您可以定义属性供自己使用,也可以使用预定义的属性,如表中列出的属性。
使用DebuggerStepThRough属性时要非常小心–它会使在应用它的方法中查找bug变得非常困难,因为您将无法在它们上单步执行或中断它们!
9.调试
调试是任何开发工作的重要组成部分。除了提供运行时环境通常不透明方面的可见性外,调试器还可以侵入运行时环境,并导致应用程序的行为与在没有调试器的情况下运行时不同。
获取异常堆栈的可见性
若要查看当前框架的异常状态,可以将表达式“$Exception”添加到VisualStudioWatch窗口。此变量包含当前的异常状态,类似于在CATCH块中看到的情况,但您可以在调试器中看到它,而不实际捕获代码中的异常。
在访问器中要小心副作用。
如果属性有副作用,请考虑是否应该使用属性或调试器设置来防止调试器自动调用getter。例如,您的类可能具有如下属性:
private int remainingAccesses = 10;
private string meteredData;
public string MeteredData
{
get
{
if (remainingAccesses– > 0)
return meteredData;
return null;
}
}
第一次在调试器中查看此对象时,剩余访问将显示为值为10,MeteredData将为空。但是,如果您在剩余的Access上悬停,您将看到它的值现在是9。调试器显示的属性值改变了对象的状态。
10.优化
早计划,常量,后优化
在设计过程中设定合理的性能目标。在开发过程中,要专注于正确性而不是微观优化。经常根据你的目标来衡量你的表现。只有当你没有达到你的目标时,你才应该花费宝贵的时间去优化一个程序。
始终使用最合适的工具来进行性能的经验测量,在既可复制又尽可能类似于用户所经历的真实世界条件的条件下。
当你衡量业绩时,要小心你实际测量的内容。当度量函数所花费的时间时,您的度量是否包括函数调用或循环构造开销?
有许多关于某些构造比其他构造更快的神话。不要假设这些都是真的,试验和测量。
有时候,由于CLR优化,看起来效率低下的代码实际上可以比高效的代码运行得更快。例如,CLR优化覆盖整个数组的循环,以避免隐式的每个元素范围检查。开发人员通常在遍历数组之前计算长度:
int[] a_val = int[4000];
int len = a_val.Length;
for (int i = 0; i < len; i++)
a_val[i] = i;
通过将长度放入变量中,CLR可能无法识别模式并跳过优化。人工优化反而使性能下降。
构建字符串
如果要进行大量字符串连接,请使用SystemT.ext.StringBuilder对象来避免构建许多临时字符串对象。
与集合一起使用批处理操作
如果需要创建和填充已知大小的集合,请在创建集合时保留空间,以避免重复重新分配导致的性能和资源问题。您可以使用AddRange方法进一步提高性能,如List<T>中的那样:
Persons.AddRange(listBox.Items);
11.资源管理
垃圾收集器允许自动清除内存。即便如此,所有可使用的资源都必须被正确地处理–特别是那些不是由垃圾收集器管理的资源。
使用TRY/Finish块来确保正确地释放资源,或者让您的类实现IDisposable,并利用USING语句,这是更干净和更安全的。
using (StreamReader reader=new StreamReader(file))
{
//your code here
避免在生产代码中使用垃圾收集器
不要通过调用GC.Colect()来干扰垃圾收集器,而是关注正确释放或处理资源。在衡量性能时,当您能够正确考虑垃圾收集器的影响时,请小心运行它。
避免编写终结器
与流行的传言相反,您的类不需要一个终结器,仅仅因为它实现了IDisposable!您可以实现IDisposable,使您的类能够对任何拥有的复合实例调用Dispose,但是终结器只能在直接拥有非托管资源的类上实现。
终结器主要用于调用互操作API来释放Win 32句柄,SafeHandle更容易处理该句柄。
您不能假设您的终结器(它总是在终结器线程上运行)可以安全地与其他对象交互。这些其他对象本身也可能正在最后确定之中。
12.并发性
并发性和多线程编程是复杂而困难的事情。在向应用程序添加并发性之前,请确保您真正了解自己正在做的事情–有很多微妙之处!
多线程应用程序很难推理,并且容易受到诸如争用条件和死锁等问题的影响,这些问题通常不会影响单线程应用程序。考虑到这些风险,您应该考虑将多线程作为最后手段。如果必须有多个线程,请尽量减少同步的需要,方法是不在线程之间共享内存。如果必须同步线程,请使用尽可能高级别的同步机制。首先是最高级别,这些机制包括:
此卡太小,无法解释C#/.NET中并发的复杂性。如果您想或需要开发利用并发性的应用程序,请查看详细的文档,如O‘Reilly的“C#Cookbook中的并发性”。
使用易失性
将一个领域标记为“易失性”是一个高级特性,甚至被专家经常误解。C#编译器将确保访问字段具有获取和发布语义;这与确保对字段的所有访问都处于锁定状态不同。如果您不知道什么是获取和发布语义以及它们如何影响CPU级别的优化,那么就避免使用易失性字段。相反,可以使用更高级别的工具,例如任务并行库(TaskParallLibrary)或CexamationToken类型。
利用线程安全内置方法
标准库类型通常提供方便线程安全访问对象的方法。例如Dictionary.TryGetValue()。使用这些方法通常可以使您的代码更简洁,并且您不需要担心诸如时间检查-时间使用场景之类的数据竞争。
不要锁定“this”、字符串或其他常见的公共对象
在实现将在多线程上下文中使用的类时,要非常小心地使用锁。对此进行锁定,字符串文本或其他公共对象将阻止对锁定状态的封装,并可能导致死锁。您需要防止其他代码锁定您的实现使用的同一个对象;您最安全的选择是私有对象成员。
13.应避免的常见错误
去引用空
不正确地使用NULL是编码缺陷的常见来源,可能导致程序崩溃和其他意外行为。如果尝试访问空引用,就好像它是对对象的有效引用–例如,通过访问属性或方法–运行时将抛出NullReferenceException。
静态和动态分析工具可以帮助您在发布代码之前识别潜在的NullReferenceExceptions。在C#中,空引用通常由尚未引用对象的变量产生。NULL是可空值类型和引用类型的有效值。例如,Nullable<Int>、空委托、未订阅事件、“as”转换失败,以及许多其他情况下。
每个空引用异常都是一个bug。不要捕获NullReferenceException,而是尝试在使用对象之前测试它们是否为NULL。这也使代码更容易阅读,方法是尽量减少TRY/CATCH块。
从数据库表读取数据时,请注意缺少的值可以表示为DBNull对象,而不是空引用。不要期望它们的行为像潜在的空引用。
对十进制值使用二进制数
浮动和双表示二进制理性主义,而不是十进制理性主义,在存储十进制值时必然使用二进制近似。从十进制的角度来看,这些二进制近似具有不一致的舍入和精度–有时会导致算术运算意外的结果。由于浮点算法通常在硬件中执行,因此硬件条件会不可避免地加剧这些差异。
当十进制精度真的很重要时使用十进制–就像财务计算一样。
修改结构
一个常见的错误场景是忘记结构是值类型-意味着它们是通过值复制和传递的。例如,假设您有如下代码:
struct P { public int x; public int y; }
void M()
{
P p = whatever;
…
p.x = something;
…
N(p);
有一天,维护人员决定将代码重构为:
void M()
{
P p = whatever;
Helper(p);
N(p);
}
void Helper(P p)
{
…
p.x = something;
现在当N(P)在M()中被调用时,p有错误的值。调用Helper(P)传递p的副本,而不是对p的引用,因此Helper()中执行的突变就丢失了。相反,Helper应该返回修改后的p的副本。
意外算法
C#编译器保护您不受常量的算术溢出,但不一定是计算值。使用“检查”和“未检查”关键字,以确保您得到您想要的行为与变量。
忽略保存返回值
与结构不同,类是引用类型,方法可以在适当的地方修改引用对象。但是,并非所有对象方法实际上都修改了引用的对象;有些方法返回一个新对象。当开发人员调用后者时,他们需要记住将返回值赋值给变量,以便使用修改过的对象。在代码评审过程中,这种类型的问题常常会被忽略。有些对象,比如String,是不可变的,所以方法永远不会修改对象。即便如此,开发人员通常还是会忘记。
例如,考虑string.replace():
string label = “My name is Aloysius”;
label.Replace(“Aloysius”, “secret”);
代码打印“我的名字是Aloyius”,因为替换方法不修改字符串。
不要使迭代器/枚举数失效
在迭代集合时,要小心不要修改它。
st<Int> myItems = new List<Int>{20,25,9,14,50};
foreach(int item in myItems)
{
if (item < 10)
{
myItems.Remove(item);
// iterator is now invalid!
// you’ll get an exception on the next iteration
如果您运行这段代码,当它循环到集合中的下一项时,就会被抛出一个异常。
正确的解决方案是使用第二个列表来保存要删除的项,然后在删除时迭代该列表:
List<Int> myItems = new List<Int>{20,25,9,14,50};
List<Int> toRemove = new List<Int>();
foreach(int item in myItems)
{
if (item < 10)
{
toRemove.Add(item);
}
}
foreach(int item in toRemove)
{
或者,如果您使用的是C#3.0或更高版本,则可以使用列表<T>。请如下所示:
myInts.RemoveAll(item => (item < 10));
属性名错误
在实现属性时,请注意属性名称与类中使用的数据成员不同。在访问属性时,很容易意外地使用相同的名称并触发无限递归。
// The following code will trigger infinite recursion
private string name;
public string Name
{
get
{
return Name; // should reference “name” instead.
在重命名间接属性时也要小心。例如,WPF中的数据绑定将属性名称指定为字符串。通过不小心地更改该属性名称,您可能会无意中造成编译器无法保护的问题。
不管你是转行也好,初学也罢,进阶也可,如果你想学编程,进阶程序员~
【值得关注】我的C语言创作者中心!
C语言入门资料:
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/6780.html