前言
最近在学习Web Api框架的时候接触到了async/await,这个特性是.NET 4.5引入的,由于之前对于异步编程不是很了解,所以花费了一些时间学习一下相关的知识,并整理成这篇博客,如果在阅读的过程中发现不对的地方,欢迎大家指正。
同步编程与异步编程
通常情况下,我们写的C#代码就是同步的,运行在同一个线程中,从程序的第一行代码到最后一句代码顺序执行。而异步编程的核心是使用多线程,通过让不同的线程执行不同的任务,实现不同代码的并行运行。
前台线程与后台线程
关于多线程,早在.NET2.0时代,基础类库中就提供了Thread实现。默认情况下,实例化一个Thread创建的是前台线程,只要有前台线程在运行,应用程序的进程就一直处于运行状态,以控制台应用程序为例,在Main方法中实例化一个Thread,这个Main方法就会等待Thread线程执行完毕才退出。而对于后台线程,应用程序将不考虑其是否执行完毕,只要应用程序的主线程和前台线程执行完毕就可以退出,退出后所有的后台线程将被自动终止。来看代码应该更清楚一些:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace ConsoleApp { class Program { static void Main(string[] args) { Console.WriteLine("主线程开始"); //实例化Thread,默认创建前台线程 Thread t1 = new Thread(DoRun1); t1.Start(); //可以通过修改Thread的IsBackground,将其变为后台线程 Thread t2 = new Thread(DoRun2) { IsBackground = true }; t2.Start(); Console.WriteLine("主线程结束"); } static void DoRun1() { Thread.Sleep(500); Console.WriteLine("这是前台线程调用"); } static void DoRun2() { Thread.Sleep(1500); Console.WriteLine("这是后台线程调用"); } } }
运行上面的代码,可以看到DoRun2方法的打印信息“这是后台线程调用”将不会被显示出来,因为应用程序执行完主线程和前台线程后,就自动退出了,所有的后台线程将被自动终止。这里后台线程设置了等待1.5s,假如这个后台线程比前台线程或主线程提前执行完毕,对应的信息“这是后台线程调用”将可以被成功打印出来。
Task
.NET 4.0推出了新一代的多线程模型Task。async/await特性是与Task紧密相关的,所以在了解async/await前必须充分了解Task的使用。这里将以一个简单的Demo来看一下Task的使用,同时与Thread的创建方式做一下对比。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using System.Threading; using System.Threading.Tasks; namespace TestApp { class Program { static void Main(string[] args) { Console.WriteLine("主线程启动"); //.NET 4.5引入了Task.Run静态方法来启动一个线程 Task.Run(() => { Thread.Sleep(1000); Console.WriteLine("Task1启动"); }); //Task启动的是后台线程,假如要在主线程中等待后台线程执行完毕,可以调用Wait方法 Task task = Task.Run(() => { Thread.Sleep(500); Console.WriteLine("Task2启动"); }); task.Wait(); Console.WriteLine("主线程结束"); } } }
首先,必须明确一点是Task启动的线程是后台线程,不过可以通过在Main方法中调用task.Wait()方法,使应用程序等待task执行完毕。Task与Thread的一个重要区分点是:Task底层是使用线程池的,而Thread每次实例化都会创建一个新的线程。这里可以通过这段代码做一次验证:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using System.Threading; using System.Threading.Tasks; namespace TestApp { class Program { static void DoRun1() { Console.WriteLine("Thread Id =" + Thread.CurrentThread.ManagedThreadId); } static void DoRun2() { Thread.Sleep(50); Console.WriteLine("Task调用Thread Id =" + Thread.CurrentThread.ManagedThreadId); } static void Main(string[] args) { for (int i = 0; i < 50; i++) { new Thread(DoRun1).Start(); } for (int i = 0; i < 50; i++) { Task.Run(() => { DoRun2(); }); } //让应用程序不立即退出 Console.Read(); } } }
运行代码,可以看到DoRun1()方法每次的Thread Id都是不同的,而DoRun2()方法的Thread Id是重复出现的。我们知道线程的创建和销毁是一个开销比较大的操作,Task.Run()每次执行将不会立即创建一个新线程,而是到CLR线程池查看是否有空闲的线程,有的话就取一个线程处理这个请求,处理完请求后再把线程放回线程池,这个线程也不会立即撤销,而是设置为空闲状态,可供线程池再次调度,从而减少开销。
Task<TResult>
Task<TResult>是Task的泛型版本,这两个之间的最大不同是Task<TResult>可以有一个返回值,看一下代码应该一目了然:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using System.Threading; using System.Threading.Tasks; namespace TestApp { class Program { static void Main(string[] args) { Console.WriteLine("主线程开始"); Task task = Task.Run(() => { Thread.Sleep(1000); return Thread.CurrentThread.ManagedThreadId.ToString(); }); Console.WriteLine(task.Result); Console.WriteLine("主线程结束"); } } }
Task<TResult>的实例对象有一个Result属性,当在Main方法中调用task.Result的时候,将等待task执行完毕并得到返回值,这里的效果跟调用task.Wait()是一样的,只是多了一个返回值。
async/await 特性
经过前面的铺垫,终于迎来了这篇文章的主角async/await,还是先通过代码来感受一下这两个特性的使用。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using System.Threading; using System.Threading.Tasks; namespace TestApp { class Program { static void Main(string[] args) { Console.WriteLine("-------主线程启动-------"); Task task = GetLengthAsync(); Console.WriteLine("Main方法做其他事情"); Console.WriteLine("Task返回的值" + task.Result); Console.WriteLine("-------主线程结束-------"); } static async Task GetLengthAsync() { Console.WriteLine("GetLengthAsync Start"); string str = await GetStringAsync(); Console.WriteLine("GetLengthAsync End"); return str.Length; } static Task GetStringAsync() { return Task.Run(() => { Thread.Sleep(2000); return "finished"; }); } } }
首先来看一下async关键字。async用来修饰方法,表明这个方法是异步的,声明的方法的返回类型必须为:void或Task或Task<TResult>。返回类型为Task的异步方法中无需使用return返回值,而返回类型为Task<TResult>的异步方法中必须使用return返回一个TResult的值,如上述Demo中的异步方法返回一个int。而返回类型可为void,则是为了和事件处理程序兼容,比如下面的示例:
public Form1() { InitializeComponent(); btnDo.Click += Down; } public async void Down(object sender, EventArgs e) { btnDo.Enabled = false; string str = await Run(); labText.Text = str; btnDo.Enabled = true; } public Task Run() { return Task.Run(() => { Thread.Sleep(5000); return DateTime.Now.ToString(); }); }
再来看一下await关键字。await必须用来修饰Task或Task<TResult>,而且只能出现在已经用async关键字修饰的异步方法中。
通常情况下,async/await必须成对出现才有意义,假如一个方法声明为async,但却没有使用await关键字,则这个方法在执行的时候就被当作同步方法,这时编译器也会抛出警告提示async修饰的方法中没有使用await,将被作为同步方法使用。了解了关键字async\await的特点后,我们来看一下上述Demo在控制台会输入什么吧。
输出的结果已经很明确地告诉我们整个执行流程了。GetLengthAsync异步方法刚开始是同步执行的,所以"GetLengthAsync Start"字符串会被打印出来,直到遇到第一个await关键字,真正的异步任务GetStringAsync开始执行,await相当于起到一个标记/唤醒点的作用,同时将控制权放回给Main方法,"Main方法做其他事情"字符串会被打印出来。之后由于Main方法需要访问到task.Result,所以就会等待异步方法GetLengthAsync的执行,而GetLengthAsync又等待GetStringAsync的执行,一旦GetStringAsync执行完毕,就会回到await GetStringAsync这个点上执行往下执行,这时"GetLengthAsync End"字符串就会被打印出来。
当然,我们也可以使用下面的方法完成上面控制台的输出。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using System.Threading; using System.Threading.Tasks; namespace TestApp { class Program { static void Main(string[] args) { Console.WriteLine("-------主线程启动-------"); Task task = GetLengthAsync(); Console.WriteLine("Main方法做其他事情"); Console.WriteLine("Task返回的值" + task.Result); Console.WriteLine("-------主线程结束-------"); } static Task GetLengthAsync() { Console.WriteLine("GetLengthAsync Start"); Task task = Task.Run(() => { string str = GetStringAsync().Result; Console.WriteLine("GetLengthAsync End"); return str.Length; }); return task; } static Task GetStringAsync() { return Task.Run(() => { Thread.Sleep(2000); return "finished"; }); } } }
对比两种方法,是不是async\await关键字的原理其实就是通过使用一个线程完成异步调用吗?答案是否定的。async关键字表明可以在方法内部使用await关键字,方法在执行到await前都是同步执行的,运行到await处就会挂起,并返回到Main方法中,直到await标记的Task执行完毕,才唤醒回到await点上,继续向下执行。更深入点的介绍可以查看文章末尾的参考文献。
async/await 实际应用
微软已经对一些基础类库的方法提供了异步实现,接下来将实现一个例子来介绍一下async/await的实际应用。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using System.Threading; using System.Threading.Tasks; using System.Net; namespace TestApp { class Program { static void Main(string[] args) { Console.WriteLine("开始获取博客园首页字符数量"); Task task1 = CountCharsAsync("http://www.cnblogs.com"); Console.WriteLine("开始获取百度首页字符数量"); Task task2 = CountCharsAsync("http://www.baidu.com"); Console.WriteLine("Main方法中做其他事情"); Console.WriteLine("博客园:" + task1.Result); Console.WriteLine("百度:" + task2.Result); } static async Task CountCharsAsync(string url) { WebClient wc = new WebClient(); string result = await wc.DownloadStringTaskAsync(new Uri(url)); return result.Length; } } }
参考文献:<IIIustrated C# 2012> 关于async/await的FAQ 《深入理解C#》
// 定义
public enum status { ready, ordered, printed }
public enum statusB { ready = 1, ordered = 3, printed = 5 }
// 将字符串转化为枚举类型
public static status? ConvertStringToEnum_status(string str)
{
return str != null && Enum.IsDefined(typeof(status), str) ? (status)Enum.Parse(typeof(status), str) : null as status?;
}
// 将数字(索引值)转化为枚举类型
status s = (status)1; // 值为 status.ordered
statusB s = (statusB)1; // 值为 statusB.ready
// 将枚举类型转化为数字(索引值)
int s = (int)status.ordered; // 值为 1
int s = (int)statusB.ready; // 值为 1
// 循环
foreach (status s in Enum.GetValues(typeof(status)))
{
}
当无法序列化或需要序列化为字符串时,在定义枚举前加上:
当使用 System.Text.Json 时为:
[System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))]
当使用 Newtonsoft.Json 时为:
[Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
今天突然发现 file_put_contents 写入文件的内容缺斤少两,字符串被截断,没有完整写入到文件中。检查了文件权限,yum 也 update 了,lnmp 也 restart 了,服务器也 reboot 了,经过各种百度无果。最终发现:原来是磁盘分区满了!!
数据库设计规范是个技术含量相对低的话题,只需要对标准和规范的坚持即可做到。当系统越来越庞大,严格控制数据库的设计人员,并且有一份规范书供执行参考。在程序框架中,也有一份强制性的约定,当不遵守规范时报错误。
以下20个条款是我从一个超过1000个数据库表的大型ERP系统中提炼出来的设计约定,供参考。
1 所有的表的第一个字段是记录编号Recnum,用于数据维护
[Recnum] [decimal] (8, 0) NOT NULL IDENTITY(1, 1)
在进行数据维护的时候,我们可以直接这样写:
UPDATE Company SET Code='FLEX' WHERE Recnum=23
2 每个表增加4个必备字段,用于记录该笔数据的创建时间,创建人,最后修改人,最后修改时间
[CreatedDate] [datetime] NULL, [CreatedBy] [nvarchar] (10) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, [RevisedDate] [datetime] NULL, [RevisedBy] [nvarchar] (10) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
框架程序中会强制读取这几个字段,默认写入值。
3 主从表的主外键设计
主表用参考编号RefNo作为主键,从表用RefNo,EntryNo作为主键。RefNo是字符串类型,可用于单据编码功能中自动填写单据流水号,从表的EntryNo是行号,LineNo是SQL Server 的关键字,所以用EntryNo作为行号。
如果是三层表,则第三层表的主键依次是RefNo,EntryNo,DetailEntryNo,第三个主键用于自动增长行号。
4 设计单据状态字段
字段 | 含义 |
Posted | 过帐,已确认 |
Closed | 已完成 |
Cancelled | 已取消 |
Approved | 已批核 |
Issued | 已发料 |
Finished | 已完成 |
Suspended | 已取消 |
5 字段含义相近,把相同的单词调成前缀。
比如工作单中的成本核算,人工成本,机器成本,能源成本,用英文表示为LaborCost,MachineCost,EnergyCost
但是为了方便规组,我们把Cost调到字段的前面,于是上面三个字段命名为CostLabor,CostMachine,CostEnergy。
可读性后者要比前者好一点,Visual Studio或SQL Prompt智能感知也可帮助提高字段输入的准确率。
6 单据引用键命名 SourceRefNo SourceEntryNo
销售送货Shipment会引用到是送哪张销售单据的,可以添加如下引用键SourceRefNo,SourceEntryNo,表示送货单引用的销售单的参考编号和行号。Source开头的字段一般用于单据引用关联。
7 数据字典键设计
比如员工主档界面的员工性别Gender,我的方法是在源代码中用枚举定义。性别枚举定义如下:
public enum Gender { [StringValue("M")] [DisplayText("Male")] Male, [StringValue("F")] [DisplayText("Female")] Female }
在代码中调用枚举的通用方法,读取枚举的StringValue写入到数据库中,读取枚举的DisplayText显示在界面中。
经过这一层设计,数据库中有关字典方面的设计就规范起来了,避免了数据字典的项的增减给系统带来的问题。
8 数值类型字段长度设计
Price/Qty 数量/单价 6个小数位 nnnnnnnnnn.nnnnnn 格式 (10.6)
Amount 金额 2个小数位 nnnnnnnnnnnn.nn 格式(12.2)
Total Amt 总金额 2个小数位 nnnnnnnnnnnnnn.nn 格式(14.2)
参考编号默认16个字符长度,不够用的情况下增加到30个字符,再不够用增加到60个字符。这样可以保证每张单据的第一个参考编号输入控件看起来都是一样长度。
除非特别需求,一般而言,界面中控件的长度取自映射的数据库中字段的定义长度。
9 每个单据表头和明细各增加10个自定义字段,基础资料表增加20个自定义字段
参考供应商主档的自定义字段,自定义字段的名称统一用UserDefinedField。
ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_1] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_2] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_3] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_4] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_5] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_6] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_7] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_8] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_9] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_10] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_11] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_12] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_13] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_14] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_15] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_16] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_17] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_18] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_19] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_20] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
10 多货币(本位币)转换字段的设计
金额或单价默认是以日记帐中的货币为记录,当默认货币与本位币不同时需要同时记录下本位币的值。
销售单销售金额 SalesAmount或SalesAmt,本位币字段定义为SalesAmountLocal或SalesAmtLocal
通常是在原来的字段后面加Local表示本位币的值。
11 各种日期字段的设计
字段名称 | 含义 |
TranDate | 日期帐日期 Tran是Transaction的简写 |
PostedDate | 过帐日期 |
ClosedDate | 完成日期 |
InvoiceDate | 开发票日期 |
DueDate | 截止日期 |
ScheduleDate | 计划日期,这个字段用在不同的单据含义不同。比如销售单是指送货日期,采购单是指收货日期。 |
OrderDate | 订单日期 |
PayDate | 付款日期 |
CreatedDate | 创建日期 |
RevisedDate | 修改日期 |
SettleDate | 付款日期 |
IssueDate | 发出日期 |
ReceiptDate | 收货日期 |
ExpireDate | 过期时间 |
12 财务有关的单据包含三个标准字段
FiscalYear 财年,PeriodNo 会计期间,Period 前面二个的组合。以国外的财年为例子,FiscalYear是2015,PeriodNo是4,Period是2015/04。
欧美会计期间是从每年的4月份开始,需要注意的是会计期间与时间没有必然的联系,看到会计期间是2015/04,不一定是表示2015的4月份,它只是说这是2015财年的第四期,具体在哪个时间段需要看会计期间定义。
13 单据自动生成 DirectEntry
有些单据是由其它单据生成过来的,逻辑上应该不支持编辑。比如销售送货Shipment单会产生出仓单,出仓单应该不支持编辑,只能做过帐扣减库存操作。这时需要DirectEntry标准字段来表示。当手工创建一张出仓单时,将DirectEntry设为true,表示可编辑单据中的字段值,当由其它单据传递产生过来产生的出仓单,将DirectEntry设为false,表示不能编辑此单据。这种情况还发生在业务单据产生记帐凭证(Voucher)的功能中,如果可以修改由原始单据传递过来的数量金额等字段,则会导致与源单不匹配,给系统对帐产生困扰。
14 百分比值字段的设计
Percentage百分比值,用于折扣率,损耗率等相关比率设定的地方。推荐用数值类型表示,用脚本表示是
[ScrapRate] [decimal] (5, 2) NULL
预留两位小数,整数部分支持1-999三位数。常常是整数部分2位就可以,用3位也是为了支持一些特殊行业(物料损耗率超过100)的要求。
15 日志表记录编号LogNo字段设计
LogNo字段的设计有些巧妙,以出仓单为例子,一张出仓单有5行物料明细,每一行物料出仓都会扣减库存,再写物料进出日记帐,因为这五行物料出仓来自同一个出仓单,于是将这五行物料的日记帐中的LogNo都设为同一个值。于在查询数据时,以这个字段分组即可看到哪些物料是在同一个时间点上出仓的,对快速查询有很重要的作用。
16 基础资料表增加名称,名称长写,代用名称三个字段
比如供应商Vendor表,给它加以下三个字段:
Description 供应商名称,比如微软公司。
ExtDescription 供应商名称长写,比如电气行业的南网的全名是南方国家电网有限公司。
AltDescription 供应商名称替代名称,用在报表或是其它单据引用中。比如采购单中的供应商是用微软,还是用代用名称Microsoft,由参数(是否用代用名称)控制。
17 文件类表增加MD5 Hash字段
比如产品数据管理系统要读取图纸,单据功能中增加的附件文件,这类涉及文件读写引用的地方,考虑存放文件的MD5哈希值。文件的MD5相当于文件的唯一识别身份,在网上下载文件时,网站常常会放出文件的MD5值,以方便对比核对。当下载到本机的文件的MD5值与网站上给出的值不一致时,有可能这个文件被第三方程序修改过,不可信任。
18 数据表的主键用字符串而不是数字
比如销售单中的货币字段,是存放货币表的货币字符串值RMB/HKD/USD,还是存放货币表的数字键,1/2/3。
存放前者对于报表制作相对容易,但是修改起来相对麻烦。存放后者对修改数据容易,但对报表类或查询类操作都需要增加一个左右连接来看数字代表的货币。金蝶使用的是后者,它的BOS系统也不允许数据表之间有直接的关联,而是间接通过Id值来关联表。
在我看到的系统中,只有一个会计期间功能(财年Fiscal Year)用到数字值作主键,其余的单据全部是字符串做主键。
19 使用约定俗成的简写
模块Module 简写
简写 | 全名 |
SL | Sales 销售 |
PU | Purchasing 采购 |
IC | Inventory 仓库 |
AR | Account Receivable 应收 |
AP | Account Payable 应付 |
GL | General Ledger 总帐 |
PR | Production 生产 |
名称Name 简写
简写 | 全名 |
Uom | Unit of Measure 单位 |
Ccy | Currency 货币 |
Amt | Amount 金额 |
Qty | Quantity 数量 |
Qty Per | Quantity Per 用量 |
Std Output | Standard Output 标准产量 |
ETA | Estimated Time of Arrival 预定到达时间 |
ETD | Estimated Time of Departure 预定出发时间 |
COD | Cash On Delivery 货到付款 |
SO | Sales Order 销售单 |
PO | Purchase Order 采购单 |
20 库存单据数量状态
Qty On Hand 在手量
Qty Available 可用量
Qty On Inspect 在验数量
Qty On Commited 提交数量
Qty Reserved 预留数量
以上每个字段都有标准和行业约定的含义,不可随意修改取数方法。
错误提示:无法从其“Visible”属性的字符串表示形式“<%= true %>”创建“System.Boolean”类型的对象。
英文:Cannot create an object of type 'System.Boolean' from its string representation '<%= true %>' for the 'Visible' property.
解决办法:改为
<% if(true) { %> 内容 <% } %>
--------------------
常量:
整型,实型,布尔值,字符串型,null,undefined
变量:var
--------------------
除法运算:9/4 = 2.25 (而不是2)
--------------------
系统函数:
1,encodeURI URL编码
var urlStr=encodeURI("http://www.abc.com/?country=吴&name=z x");
结果:http://www.abc.com/?country=%E5%90%B4&name=z%20x
2,decodeURI URL解码
3,parseInt 将字符串按指定进制转换成一个整数,参数二表示进制
parseInt("32",10) 结果:32
parseInt("3c2",10) 结果:3
parseInt("c32",10) 结果:NaN
parseInt("0x18",10) 结果:0
parseInt("12",16) 结果18
4,parseFloat 将字符串转成小数
parseFloat("2.5") 结果:2.5
parseFloat("2.c5") 结果:2
parseFloat("c2.5") 结果:NaN
5,isNaN 用于检测parseInt和parseFloat返回是否为NaN
返回true/false
6,escape 编码
非ASCII替换为%xx
7,unescape 解码
8,eval 将字符串作为JS表达式执行,例
for(var i=0;i<3;i++) eval("var a"+i+"="+i);
相当于:
var a0=0; var a1=1; var a2=2;
--------------------
对象
1,Object
2,String
方法:
indexOf,
lastIndexOf,
match,使用正则表达式模式对字符串执行搜索,返回包含该搜索结果的数组
replace,
search,使用正则表达式搜索,第一个匹配的字符的位置
slice,截字符串:参数一,开始位置,参数二,结束位置(不指定或为-1时表示末位置)
split,返回一个字符串按照某种分隔标志符拆分为若干子字符串时所产生的字符串数组,分隔符可以是多个字符或一个正则表达式,它不作为任何数组元素的一部分返回。
substr,截字符串:参数一,开始位置,参数二,长度
toLowerCase,
toUpperCase,
3,Math(不能用new创建)
方法:
abs,绝对值
sin,cos,正余弦
asin,acos,反正余弦
random,返回介于0~1之间的伪随机数
例:var num = Math.randow();
4,Date
var currentTime=new Date();
//var currentTime=new Date(2002,3,4);
var strDate=currentTime.getYear()+"年";
strDate+=currentTime.getMonth()+"月";
strDate+=currentTime.getDate()+"日";
strDate+=currentTime.getHours()+":";
strDate+=currentTime.getMinutes()+":";
strDate+=currentTime.getSeconds()+" ";
strDate+=currentTime.getMilliseconds();
alert(strDate);
结果:2008年1月19日15:27:10 518
----------------------
数组
1,
var arr=["abc",123,'abc',,3.4];
for(var i=0;i<arr.length;i++)
{
alert(arr[i]);
}
2,用对象的方式实现数组
function MyArray(){this.length=arguments.length;for(var i=0;....
3,Array对象
var arr=new Array(2.4,"abc",2);
arr.sort(); //排序
alert的结果为 2 2,4 abc
-----------------------
自从有了IP数据库这种东西,QQ外挂的显示IP功能也随之而生,本人见识颇窄,是否还有其他应用不得而知,不过,IP数据库确实是个不错的东西。如今网络上最流行的IP数据库我想应该是纯真版的(说错了也不要扁我),迄今为止其IP记录条数已经接近30000,对于有些IP甚至能精确到楼层,不亦快哉。2004年4、5月间,正逢LumaQQ破土动工,为了加上这个人人都喜欢,但是好像人人都不知道为什么喜欢的显IP功能,我也采用了纯真版IP数据库,它的优点是记录多,查询速度快,它只用一个文件QQWry.dat就包含了所有记录,方便嵌入到其他程序中,也方便升级。
基本结构
QQWry.dat文件在结构上分为3块:文件头,记录区,索引区。一般我们要查找IP时,先在索引区查找记录偏移,然后再到记录区读出信息。由于记录区的记录是不定长的,所以直接在记录区中搜索是不可能的。由于记录数比较多,如果我们遍历索引区也会是有点慢的,一般来说,我们可以用二分查找法搜索索引区,其速度比遍历索引区快若干数量级。图1是QQWry.dat的文件结构图。
图1. QQWry.dat文件结构
要注意的是,QQWry.dat里面全部采用了little-endian字节序
一. 了解文件头
QQWry.dat的文件头只有8个字节,其结构非常简单,首四个字节是第一条索引的绝对偏移,后四个字节是最后一条索引的绝对偏移。
二. 了解记录区
每条IP记录都由国家和地区名组成,国家地区在这里并不是太确切,因为可能会查出来“清华大学计算机系”之类的,这里清华大学就成了国家名了,所以这个国家地区名和IP数据库制作的时候有关系。所以记录的格式有点像QName,有一个全局部分和局部部分组成,我们这里还是沿用国家名和地区名的说法。
于是我们想象着一条记录的格式应该是: [IP地址][国家名][地区名],当然,这个没有什么问题,但是这只是最简单的情况。很显然,国家名和地区名可能会有很多的重复,如果每条记录都保存一个完整的名称拷贝是非常不理想的,所以我们就需要重定向以节省空间。所以为了得到一个国家名或者地区名,我们就有了两个可能:第一就是直接的字符串表示的国家名,第二就是一个4字节的结构,第一个字节表明了重定向的模式,后面3个字节是国家名或者地区名的实际偏移位置。对于国家名来说,情况还可能更复杂些,因为这样的重定向最多可能有两次。
那么什么是重定向模式?根据上面所说,一条记录的格式是[IP地址][国家记录][地区记录],如果国家记录是重定向的话,那么地区记录是有可能没有的,于是就有了两种情况,我管他叫做模式1和模式2。我们对这些格式的情况举图说明:
图2. IP记录的最简单形式
图2表示了最简单的IP记录格式,我想没有什么可以解释的
图3. 重定向模式1
图3演示了重定向模式1的情况。我们看到在模式1的情况下,地区记录也跟着国家记录走了,在IP地址之后只剩下了国家记录的4字节,后面3个字节构成了一个指针,指向了实际的国家名,然后又跟着地址名。模式1的标识字节是0x01。
图4. 重定向模式2
图4演示了重定向模式2的情况。我们看到了在模式2的情况下(其标识字节是0x02),地区记录没有跟着国家记录走,因此在国家记录之后4个字节之后还是有地区记录。我想你已经明白了模式1和模式2的区别,即:模式1的国家记录后面不会再有地区记录,模式2的国家记录后会有地区记录。下面我们来看一下更复杂的情况。
图5. 混和情况1
图5演示了当国家记录为模式1的时候可能出现的更复杂情况,在这种情况下,重定向指向的位置仍然是个重定向,不过第二次重定向为模式2。大家不用担心,没有模式3了,这个重定向也最多只有两次,并且如果发生了第二次重定向,则其一定为模式2,而且这种情况只会发生在国家记录上,对于地区记录,模式1和模式2是一样的,地区记录也不会发生2次重定向。不过,这个图还可以更复杂,如图7:
图6. 混和情况2
图6是模式1下最复杂的混和情况,不过我想应该也很好理解,只不过地区记录也来重定向而已,有一点我要提醒你,如果重定向的地址是0,则表示未知的地区名。
所以我们总结如下:一条IP记录由[IP地址][国家记录][地区记录]组成,对于国家记录,可以有三种表示方式:字符串形式,重定向模式1和重定向模式2。对于地区记录,可以有两种表示方式:字符串形式和重定向,另外有一条规则:重定向模式1的国家记录后不能跟地区记录。按照这个总结,在这些方式中合理组合,就构成了IP记录的所有可能情况。
设计的理由
在我们继续去了解索引区的结构之前,我们先来了解一下为何记录区的结构要如此设计。我想你可能想到了答案:字符串重用。没错,在这种结构下,对于一个国家名和地区名,我只需要保存其一次就可以了。我们举例说明,为了表示方便,我们用小写字母代表IP记录,C表示国家名,A表示地区名:
有两条记录a(C1, A1), b(C2, A2),如果C1 = C2, A1 = A2,那么我们就可以使用图3显示的结构来实现重用
有三条记录a(C1, A1), b(C2, A2), c(C3, A3),如果C1 = C2, A2 = A3,现在我们想存储记录b,那么我们可以用图6的结构来实现重用
有两条记录a(C1, A1), b(C2, A2),如果C1 = C2,现在我们想存储记录b,那么我们可以采用模式2表示C2,用字符串表示A2
你可以举出更多的情况,你也会发现在这种结构下,不同的字符串只需要存储一次。
了解索引区
在"了解文件头"部分,我们说明了文件头实际上是两个指针,分别指向了第一条索引和最后一条索引的绝对偏移。如图8所示:
图8. 文件头指向索引区图示
实在是很简单,不是吗?从文件头你就可以定位到索引区,然后你就可以开始搜索IP了!每条索引长度为7个字节,前4个字节是起始IP地址,后三个字节就指向了IP记录。这里有些概念需要说明一下,什么是起始IP,那么有没有结束IP?假设有这么一条记录:166.111.0.0 - 166.111.255.255,那么166.111.0.0就是起始IP,166.111.255.255就是结束IP,结束IP就是IP记录中的那头4个字节,这下你应该就清楚了吧。于是乎,每条索引配合一条记录,构成了一个IP范围,如果你要查找166.111.138.138所在的位置,你就会发现166.111.138.138落在了166.111.0.0- 166.111.255.255 这个范围内,那么你就可以顺着这条索引去读取国家和地区名了。那么我们给出一个最详细的图解吧:
图9. 文件详细结构
现在一切都清楚了是不是?也许还有一点你不清楚,QQWry.dat的版本信息存在哪里呢? 答案是:最后一条IP记录实际上就是版本信息,最后一条记录显示出来就是这样:255.255.255.0255.255.255.255 纯真网络 2004年6月25日IP数据。OK,到现在你应该全部清楚了。
Demo
下一步:我给出一个读取IP记录的程序片断,此片断摘录自LumaQQ源文件edu.tsinghua.lumaqq.IPSeeker.java,如果你有兴趣,可以下载源代码详细看看。
/** *//**
* 给定一个ip国家地区记录的偏移,返回一个IPLocation结构
* @param offset 国家记录的起始偏移
* @return IPLocation对象
*/
private IPLocation getIPLocation(long offset) {
try {
// 跳过4字节ip
ipFile.seek(offset + 4);
// 读取第一个字节判断是否标志字节
byte b = ipFile.readByte();
if(b == REDIRECT_MODE_1) {
// 读取国家偏移
long countryOffset = readLong3();
// 跳转至偏移处
ipFile.seek(countryOffset);
// 再检查一次标志字节,因为这个时候这个地方仍然可能是个重定向
b = ipFile.readByte();
if(b == REDIRECT_MODE_2) {
loc.country = readString(readLong3());
ipFile.seek(countryOffset + 4);
} else
loc.country = readString(countryOffset);
// 读取地区标志
loc.area = readArea(ipFile.getFilePointer());
} else if(b == REDIRECT_MODE_2) {
loc.country = readString(readLong3());
loc.area = readArea(offset + 8);
} else {
loc.country = readString(ipFile.getFilePointer() - 1);
loc.area = readArea(ipFile.getFilePointer());
}
return loc;
} catch (IOException e) {
return null;
}
}
/** *//**
* 从offset偏移开始解析后面的字节,读出一个地区名
* @param offset 地区记录的起始偏移
* @return 地区名字符串
* @throws IOException 地区名字符串
*/
private String readArea(long offset) throws IOException {
ipFile.seek(offset);
byte b = ipFile.readByte();
if(b == REDIRECT_MODE_1 || b == REDIRECT_MODE_2) {
long areaOffset = readLong3(offset + 1);
if(areaOffset == 0)
return LumaQQ.getString("unknown.area");
else
return readString(areaOffset);
} else
return readString(offset);
}
/** *//**
* 从offset位置读取3个字节为一个long,因为java为big-endian格式,所以没办法
* 用了这么一个函数来做转换
* @param offset 整数的起始偏移
* @return 读取的long值,返回-1表示读取文件失败
*/
private long readLong3(long offset) {
long ret = 0;
try {
ipFile.seek(offset);
ipFile.readFully(b3);
ret |= (b3[0] & 0xFF);
ret |= ((b3[1] << 8) & 0xFF00);
ret |= ((b3[2] << 16) & 0xFF0000);
return ret;
} catch (IOException e) {
return -1;
}
}
/** *//**
* 从当前位置读取3个字节转换成long
* @return 读取的long值,返回-1表示读取文件失败
*/
private long readLong3() {
long ret = 0;
try {
ipFile.readFully(b3);
ret |= (b3[0] & 0xFF);
ret |= ((b3[1] << 8) & 0xFF00);
ret |= ((b3[2] << 16) & 0xFF0000);
return ret;
} catch (IOException e) {
return -1;
}
}
/** *//**
* 从offset偏移处读取一个以0结束的字符串
* @param offset 字符串起始偏移
* @return 读取的字符串,出错返回空字符串
*/
private String readString(long offset) {
try {
ipFile.seek(offset);
int i;
for(i = 0, buf[i] = ipFile.readByte(); buf[i] != 0; buf[++i] = ipFile.readByte());
if(i != 0)
return Utils.getString(buf, 0, i, "GBK");
} catch (IOException e) {
log.error(e.getMessage());
}
return "";
}
代码并不复杂,getIPLocation是主要方法,它检查国家记录格式,并针对字符串形式,模式1,模式2采用不同的代码,readArea则相对简单,因为只有字符串和重定向两种情况需要处理。
总结
纯真IP数据库的结构使得查找IP简单迅速,不过你想要编辑它却是比较麻烦的,我想应该需要专门的工具来生成QQWry.dat文件,由于其文件格式的限制,你要直接添加IP记录就不容易了。不过,能查到IP已经很开心了,希望纯真记录越来越多~。
LumaQQ is a Java QQ client which has a reusablepure Java core and SWT-based GUI
string chkCode = string.Empty;
//颜色列表,用于验证码、噪线、噪点
//Color[] color ={Color.White, Color.Yellow, Color.LightBlue, Color.LightGreen,
// Color.Orange, Color.LightCyan, Color.LightPink,
// Color.LightSalmon };
Color[] color = {Color.Black, Color.Blue, Color.Red, Color.DarkViolet,
Color.Chocolate, Color.DarkGreen, Color.DeepSkyBlue,
Color.DarkCyan };
//字体列表,用于验证码
string[] font = {"Times New Roman", "MS Mincho", "Book Antiqua", "Gungsuh",
"PMingLiU", "Impact" };
//验证码的字符集,去掉了一些容易混淆的字符
char[] character ={'2', '3', '4', '5', '6', '8', '9', 'A', 'B', 'C', 'D', 'E',
'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'R', 'S', 'T',
'W', 'X', 'Y' };
Random rnd = new Random();
//生成验证码字符串
for (int i = 0; i < 4; i++)
{
chkCode += character[rnd.Next(character.Length)];
}
//把验证码存入session
Session["chkCode"] = chkCode;
Bitmap bmp = new Bitmap(65, 20);
Graphics g = Graphics.FromImage(bmp);
g.Clear(Color.Transparent);//背景色
//画噪线
for (int i = 0; i < 0; i++)
{
int x1 = rnd.Next(65);
int y1 = rnd.Next(20);
int x2 = rnd.Next(65);
int y2 = rnd.Next(20);
Color clr = color[rnd.Next(color.Length)];
g.DrawLine(new Pen(clr), x1, y1, x2, y2);
}
//画验证码字符串
for (int i = 0; i < chkCode.Length; i++)
{
string fnt = font[rnd.Next(font.Length)];
Font ft = new Font(fnt, 15);
Color clr = color[rnd.Next(color.Length)];
g.DrawString(chkCode[i].ToString(), ft, new SolidBrush(clr),
(float)i * 15 + 2, (float)0);
}
//画噪点
for (int i = 0; i < 50; i++)
{
int x = rnd.Next(bmp.Width);
int y = rnd.Next(bmp.Height);
Color clr = color[rnd.Next(color.Length)];
bmp.SetPixel(x, y, clr);
}
//清除该页输出缓存,设置该页无缓存
Response.Buffer = true;
Response.ExpiresAbsolute = System.DateTime.Now.AddMilliseconds(0);
Response.Expires = 0;
Response.CacheControl = "no-cache";
Response.AppendHeader("Pragma", "No-Cache");
//将验证码图片写入内存流,并将其以 "image/Png" 格式输出
MemoryStream ms = new MemoryStream();
try
{
bmp.Save(ms, ImageFormat.Png);
Response.ClearContent();
Response.ContentType = "image/Png";
Response.BinaryWrite(ms.ToArray());
}
finally
{
//显式释放资源
bmp.Dispose();
g.Dispose();
}
我们在用数据控件的时候,一般会在 .aspx 页面生成如单向绑定
或双向绑定
之类的绑定代码,如果我们必需对它进行进一步处理,譬如取得的“产品分类的ID”,我想显示为“产品分类的名称”,或者格式化时间显示
等等。
如果只是连接字符串可以这样处理
如果是通过一个方法去改变绑定值,则可以:
这里可以套用任何自定义的方法。更可以实现绑定两个数据库字段的实现:
{
if (archive.Trim() != "")
{
return "/xsdn/archive/" + archive + ".aspx";
}
else
{
return "/xsdn/"+id+".aspx";
}
}
一、一维:
int[] numbers = new int[]{1,2,3,4,5,6}; //不定长
int[] numbers = new int[3]{1,2,3};//定长
二、多维
int[,] numbers = new int[,]{{1,2,3},{1,2,3}}; //不定长
int[,] numbers = new int[2,2]{{1,2},{1,2}}; //定长
三、例子
A:int[] mf1=new int[6];
//注意初始化数组的范围,或者指定初值; //包含6个元素的一维整数数组,初值1,2,3,4,5,6
int[] mf2=new int[6]{1,2,3,4,5,6};
B://一维字符串数组,如果提供了初始值设定项,则还可以省略 new 运算符
string[] mf3={"c","c++","c#"};
C://一维对象数组
Object[] mf4 = new Object[5] { 26, 27, 28, 29, 30 };
D://二维整数数组,初值mf5[0,0]=1,mf5[0,1]=2,mf5[1,0]=3,mf5[1,1]=4
int[,] mf5=new int[,]{{1,2},{3,4}};
E://6*6的二维整型数组
int[,] mf6=new mf[6,6];
四、取得数组元素个数:
int b;
b = sizeof (a)/sizeof (*a);